Spaces:
Running
Running
Deploy vectorization service from GitHub da30902c39d4577af0609f4da9aa94e293a64033
Browse files- README.md +9 -3
- VERSION +1 -1
- app/batching.py +30 -8
- app/models.py +1 -0
README.md
CHANGED
|
@@ -40,7 +40,9 @@ curl -sS -X POST 'http://localhost:7860/colbert/encode' \
|
|
| 40 |
-d '{"text":"שלום עולם","type":"passage"}'
|
| 41 |
```
|
| 42 |
|
| 43 |
-
E5 keeps `query: ` / `passage: ` prefixes, attention-mask-aware mean pooling, **no extra L2 normalization**. Qwen uses its official query instruction + last-token pooling + L2 normalization; documents have no instruction. PyLate mLateOn handles query and passage separately, retaining token vectors and their model-native normalization. Token limits
|
|
|
|
|
|
|
| 44 |
for a free inference slot (40 seconds by default). A full queue or expired wait
|
| 45 |
returns HTTP 429 with `Retry-After`. Configure `INFERENCE_MAX_WAITERS` and
|
| 46 |
`INFERENCE_WAIT_SECONDS` as needed. Dify's OpenAI-compatible embedding provider
|
|
@@ -63,7 +65,10 @@ to the base URL ending in `/openai/v1`, not to the Voyage `/v1` URL.
|
|
| 63 |
- Dense E5 and Qwen3 only. ColBERT retains `/colbert/encode` and Voyage extension.
|
| 64 |
- **Crucial:** this endpoint forwards raw input to the tokenizer **without adding any
|
| 65 |
query/document instructions**. Supply prefixes in the client configuration.
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
For E5 configure in Dify:
|
| 69 |
- Document prefix: `passage:`
|
|
@@ -72,7 +77,8 @@ For E5 configure in Dify:
|
|
| 72 |
which is not this server's limit). Dify inserts a space after the prefix.
|
| 73 |
|
| 74 |
The OpenAI endpoint accepts up to 1000 strings per request, subject to
|
| 75 |
-
65,536 total input tokens
|
|
|
|
| 76 |
requests may still exceed the client's timeout; prefer batches of 8–16 on CPU.
|
| 77 |
|
| 78 |
For Qwen3:
|
|
|
|
| 40 |
-d '{"text":"שלום עולם","type":"passage"}'
|
| 41 |
```
|
| 42 |
|
| 43 |
+
E5 keeps `query: ` / `passage: ` prefixes, attention-mask-aware mean pooling, **no extra L2 normalization**. Qwen uses its official query instruction + last-token pooling + L2 normalization; documents have no instruction. PyLate mLateOn handles query and passage separately, retaining token vectors and their model-native normalization. Token limits include model markers and special tokens. The legacy and Voyage
|
| 44 |
+
APIs reject overlong inputs (HTTP 422); only the OpenAI-compatible E5
|
| 45 |
+
endpoint truncates inputs exceeding 512 tokens, logging affected batch indices. A single CPU inference runs at a time; up to 16 additional HTTP requests wait
|
| 46 |
for a free inference slot (40 seconds by default). A full queue or expired wait
|
| 47 |
returns HTTP 429 with `Retry-After`. Configure `INFERENCE_MAX_WAITERS` and
|
| 48 |
`INFERENCE_WAIT_SECONDS` as needed. Dify's OpenAI-compatible embedding provider
|
|
|
|
| 65 |
- Dense E5 and Qwen3 only. ColBERT retains `/colbert/encode` and Voyage extension.
|
| 66 |
- **Crucial:** this endpoint forwards raw input to the tokenizer **without adding any
|
| 67 |
query/document instructions**. Supply prefixes in the client configuration.
|
| 68 |
+
For **E5 only**, inputs exceeding 512 tokens are truncated from the right
|
| 69 |
+
to 512 tokens, including the prefix and special tokens. Short inputs are
|
| 70 |
+
unchanged; truncation is logged with the input index and original token count.
|
| 71 |
+
Qwen inputs are not truncated. Unsupported dimensions return an error.
|
| 72 |
|
| 73 |
For E5 configure in Dify:
|
| 74 |
- Document prefix: `passage:`
|
|
|
|
| 77 |
which is not this server's limit). Dify inserts a space after the prefix.
|
| 78 |
|
| 79 |
The OpenAI endpoint accepts up to 1000 strings per request, subject to
|
| 80 |
+
65,536 total **processed** input tokens (after E5 truncation) and internal
|
| 81 |
+
padding-aware microbatches. Large
|
| 82 |
requests may still exceed the client's timeout; prefer batches of 8–16 on CPU.
|
| 83 |
|
| 84 |
For Qwen3:
|
VERSION
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
|
|
|
|
| 1 |
+
da30902c39d4577af0609f4da9aa94e293a64033
|
app/batching.py
CHANGED
|
@@ -5,9 +5,13 @@ No model initialization, truncation, or extra E5 normalization occurs here.
|
|
| 5 |
"""
|
| 6 |
from __future__ import annotations
|
| 7 |
|
|
|
|
|
|
|
| 8 |
from app.embedding import EmbeddingResult, TooManyTokens
|
| 9 |
from app.models import ColbertResult, validate_vector, INSTRUCTION
|
| 10 |
|
|
|
|
|
|
|
| 11 |
|
| 12 |
def _split_by_padding_cost(counts, token_budget, max_size):
|
| 13 |
"""Partition ordered inputs, accounting for batch padding to longest text."""
|
|
@@ -33,7 +37,7 @@ def _token_count(tokenizer, text):
|
|
| 33 |
return len(encoded["input_ids"])
|
| 34 |
|
| 35 |
|
| 36 |
-
def _dense_batch(engine, texts, kind, is_qwen, raw=False):
|
| 37 |
if raw:
|
| 38 |
# OpenAI-compatible callers provide preformatted text; never add prompts.
|
| 39 |
strings = list(texts)
|
|
@@ -53,7 +57,14 @@ def _dense_batch(engine, texts, kind, is_qwen, raw=False):
|
|
| 53 |
counts = [_token_count(engine.tokenizer, text) for text in strings]
|
| 54 |
for index, count in enumerate(counts):
|
| 55 |
if count > engine.max_tokens:
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
# Bound total work in one HTTP request (not the model context limit).
|
| 58 |
if sum(counts) > 65536:
|
| 59 |
raise ValueError("Batch exceeds 65536 total input tokens")
|
|
@@ -61,10 +72,17 @@ def _dense_batch(engine, texts, kind, is_qwen, raw=False):
|
|
| 61 |
results = [None] * len(texts)
|
| 62 |
for group in _split_by_padding_cost(counts, budget, max_size):
|
| 63 |
inputs = [strings[i] for i in group]
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
with engine.torch.inference_mode():
|
| 69 |
hidden = engine.model(**batch).last_hidden_state
|
| 70 |
if is_qwen:
|
|
@@ -126,8 +144,11 @@ def _colbert_batch(engine, texts, kind):
|
|
| 126 |
return results
|
| 127 |
|
| 128 |
|
| 129 |
-
def batch_encode(engine, texts, kind, is_qwen=False, is_colbert=False,
|
|
|
|
| 130 |
"""Native tensor batch for real models, single-item fallback for test doubles."""
|
|
|
|
|
|
|
| 131 |
if is_colbert:
|
| 132 |
if not hasattr(engine, "model") or not hasattr(engine.model, "tokenizer"):
|
| 133 |
return [engine.encode(text, kind) for text in texts]
|
|
@@ -136,4 +157,5 @@ def batch_encode(engine, texts, kind, is_qwen=False, is_colbert=False, raw=False
|
|
| 136 |
if raw:
|
| 137 |
raise RuntimeError("Raw embeddings require the loaded model and tokenizer")
|
| 138 |
return [engine.encode(text, kind) for text in texts]
|
| 139 |
-
return _dense_batch(engine, texts, kind, is_qwen, raw=raw
|
|
|
|
|
|
| 5 |
"""
|
| 6 |
from __future__ import annotations
|
| 7 |
|
| 8 |
+
import logging
|
| 9 |
+
|
| 10 |
from app.embedding import EmbeddingResult, TooManyTokens
|
| 11 |
from app.models import ColbertResult, validate_vector, INSTRUCTION
|
| 12 |
|
| 13 |
+
log = logging.getLogger(__name__)
|
| 14 |
+
|
| 15 |
|
| 16 |
def _split_by_padding_cost(counts, token_budget, max_size):
|
| 17 |
"""Partition ordered inputs, accounting for batch padding to longest text."""
|
|
|
|
| 37 |
return len(encoded["input_ids"])
|
| 38 |
|
| 39 |
|
| 40 |
+
def _dense_batch(engine, texts, kind, is_qwen, raw=False, truncate_long=False):
|
| 41 |
if raw:
|
| 42 |
# OpenAI-compatible callers provide preformatted text; never add prompts.
|
| 43 |
strings = list(texts)
|
|
|
|
| 57 |
counts = [_token_count(engine.tokenizer, text) for text in strings]
|
| 58 |
for index, count in enumerate(counts):
|
| 59 |
if count > engine.max_tokens:
|
| 60 |
+
if not truncate_long:
|
| 61 |
+
raise TooManyTokens(count, engine.max_tokens, input_index=index)
|
| 62 |
+
log.warning(
|
| 63 |
+
"OpenAI embeddings truncated: model=e5 input_index=%d "
|
| 64 |
+
"original_tokens=%d used_tokens=%d",
|
| 65 |
+
index, count, engine.max_tokens,
|
| 66 |
+
)
|
| 67 |
+
counts[index] = engine.max_tokens
|
| 68 |
# Bound total work in one HTTP request (not the model context limit).
|
| 69 |
if sum(counts) > 65536:
|
| 70 |
raise ValueError("Batch exceeds 65536 total input tokens")
|
|
|
|
| 72 |
results = [None] * len(texts)
|
| 73 |
for group in _split_by_padding_cost(counts, budget, max_size):
|
| 74 |
inputs = [strings[i] for i in group]
|
| 75 |
+
tokenizer_options = {
|
| 76 |
+
"return_tensors": "pt",
|
| 77 |
+
"padding": True,
|
| 78 |
+
"truncation": truncate_long,
|
| 79 |
+
"add_special_tokens": True,
|
| 80 |
+
}
|
| 81 |
+
if truncate_long:
|
| 82 |
+
# Hugging Face preserves special tokens inside max_length and
|
| 83 |
+
# truncates from the right, keeping the leading query:/passage: prefix.
|
| 84 |
+
tokenizer_options["max_length"] = engine.max_tokens
|
| 85 |
+
batch = engine.tokenizer(inputs, **tokenizer_options)
|
| 86 |
with engine.torch.inference_mode():
|
| 87 |
hidden = engine.model(**batch).last_hidden_state
|
| 88 |
if is_qwen:
|
|
|
|
| 144 |
return results
|
| 145 |
|
| 146 |
|
| 147 |
+
def batch_encode(engine, texts, kind, is_qwen=False, is_colbert=False,
|
| 148 |
+
raw=False, truncate_long=False):
|
| 149 |
"""Native tensor batch for real models, single-item fallback for test doubles."""
|
| 150 |
+
if truncate_long and (not raw or is_qwen or is_colbert):
|
| 151 |
+
raise ValueError("Automatic truncation is only supported for raw E5 input")
|
| 152 |
if is_colbert:
|
| 153 |
if not hasattr(engine, "model") or not hasattr(engine.model, "tokenizer"):
|
| 154 |
return [engine.encode(text, kind) for text in texts]
|
|
|
|
| 157 |
if raw:
|
| 158 |
raise RuntimeError("Raw embeddings require the loaded model and tokenizer")
|
| 159 |
return [engine.encode(text, kind) for text in texts]
|
| 160 |
+
return _dense_batch(engine, texts, kind, is_qwen, raw=raw,
|
| 161 |
+
truncate_long=truncate_long)
|
app/models.py
CHANGED
|
@@ -190,6 +190,7 @@ class ModelRegistry:
|
|
| 190 |
raise ValueError("OpenAI embeddings only support dense E5 and Qwen models")
|
| 191 |
return self._run(lambda: batch_encode(
|
| 192 |
engine, texts, "passage", is_qwen=is_qwen, raw=True,
|
|
|
|
| 193 |
))
|
| 194 |
|
| 195 |
def colbert_encode(self, text, kind):
|
|
|
|
| 190 |
raise ValueError("OpenAI embeddings only support dense E5 and Qwen models")
|
| 191 |
return self._run(lambda: batch_encode(
|
| 192 |
engine, texts, "passage", is_qwen=is_qwen, raw=True,
|
| 193 |
+
truncate_long=(canonical_model == E5_ID),
|
| 194 |
))
|
| 195 |
|
| 196 |
def colbert_encode(self, text, kind):
|