SmartDataAlex commited on
Commit
c6afbdf
·
verified ·
1 Parent(s): 3adb6fd

Deploy vectorization service from GitHub da30902c39d4577af0609f4da9aa94e293a64033

Browse files
Files changed (4) hide show
  1. README.md +9 -3
  2. VERSION +1 -1
  3. app/batching.py +30 -8
  4. 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 are checked *including model markers and special tokens*, without silent truncation. Exceeding limits or using invalid input returns HTTP 422. A single CPU inference runs at a time; up to 16 additional HTTP requests wait
 
 
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
- It never silently truncates, and unsupported dimensions return an error.
 
 
 
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 and internal padding-aware microbatches. Large
 
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
- 3807fb18ead86e70413d1081aa3bedb1935f6e61
 
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
- raise TooManyTokens(count, engine.max_tokens, input_index=index)
 
 
 
 
 
 
 
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
- batch = engine.tokenizer(
65
- inputs, return_tensors="pt", padding=True,
66
- truncation=False, add_special_tokens=True,
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, raw=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):