Text Ranking
sentence-transformers
Safetensors
Transformers
multilingual
t5gemma2
text2text-generation
reranker
encoder-decoder
FBNL
Retrieval
RAG
cosyy commited on
Commit
77c420e
·
verified ·
1 Parent(s): a175394

Update vLLM support and remove build artifacts

Browse files
vllm_support/README.md CHANGED
@@ -1,7 +1,7 @@
1
- # KaLM-Reranker-V1-Small vLLM Support
2
 
3
  This directory contains the experimental vLLM 0.19.1 adapter for
4
- `KaLM-Embedding/KaLM-Reranker-V1-Small`. It supports offline Python and CLI
5
  reranking plus an optional FastAPI service.
6
 
7
  The adapter does not modify or retrain the checkpoint. It reads the original
@@ -35,9 +35,9 @@ conda activate kalm-vllm
35
  pip install "vllm==0.19.1" "transformers==5.6.2"
36
  pip install "fastapi>=0.136,<0.137" "uvicorn>=0.46,<0.47"
37
 
38
- hf download KaLM-Embedding/KaLM-Reranker-V1-Small \
39
- --local-dir ./KaLM-Reranker-V1-Small
40
- pip install ./KaLM-Reranker-V1-Small/vllm_support --no-deps
41
  export VLLM_PLUGINS=kalm_t5gemma2
42
  ```
43
 
@@ -45,10 +45,10 @@ The model can also be loaded directly by its Hugging Face ID. In that case,
45
  only download the `vllm_support` directory before installing the plugin:
46
 
47
  ```bash
48
- hf download KaLM-Embedding/KaLM-Reranker-V1-Small \
49
  --include "vllm_support/**" \
50
- --local-dir ./KaLM-Reranker-V1-Small
51
- pip install ./KaLM-Reranker-V1-Small/vllm_support --no-deps
52
  ```
53
 
54
  ## Offline Python API
@@ -64,7 +64,7 @@ documents = [
64
 
65
  pairs = [(query, document) for document in documents]
66
  with KaLMVLLMReranker(
67
- "KaLM-Embedding/KaLM-Reranker-V1-Small",
68
  query_max_length=512,
69
  document_max_length=1024,
70
  encoder_chunk_size=4,
@@ -97,7 +97,7 @@ Score JSONL input:
97
 
98
  ```bash
99
  kalm-vllm-rerank \
100
- --input-jsonl ./KaLM-Reranker-V1-Small/vllm_support/examples/sample_pairs.jsonl \
101
  --output-jsonl ./scores.jsonl \
102
  --return-margin
103
  ```
@@ -114,7 +114,7 @@ Start one model instance:
114
  kalm-vllm-serve \
115
  --host 0.0.0.0 \
116
  --port 8000 \
117
- --model KaLM-Embedding/KaLM-Reranker-V1-Small \
118
  --encoder-chunk-size 4
119
  ```
120
 
@@ -123,7 +123,7 @@ variables:
123
 
124
  ```bash
125
  CUDA_VISIBLE_DEVICES=0 PORT=8000 \
126
- ./KaLM-Reranker-V1-Small/vllm_support/examples/start_online_server.sh
127
  ```
128
 
129
  In a second terminal, check health and send built-in demo requests:
@@ -140,7 +140,7 @@ one query against multiple documents:
140
  ```bash
141
  kalm-vllm-client \
142
  --endpoint rerank \
143
- --json-file ./KaLM-Reranker-V1-Small/vllm_support/examples/rerank_request.json \
144
  --return-margin \
145
  --top-k 10
146
  ```
@@ -150,7 +150,7 @@ Use `/score` for a batch of independent query-document pairs:
150
  ```bash
151
  kalm-vllm-client \
152
  --endpoint score \
153
- --json-file ./KaLM-Reranker-V1-Small/vllm_support/examples/score_request.json \
154
  --return-margin
155
  ```
156
 
@@ -247,7 +247,7 @@ Reinstall the package and ensure the environment variable includes its entry
247
  point name:
248
 
249
  ```bash
250
- pip install ./KaLM-Reranker-V1-Small/vllm_support --no-deps --force-reinstall
251
  export VLLM_PLUGINS=kalm_t5gemma2
252
  ```
253
 
@@ -258,7 +258,7 @@ sensitive.
258
 
259
  **The tokenizer check fails**
260
 
261
- Confirm that the tokenizer belongs to this Small checkpoint. The adapter
262
  requires `yes -> 4443` and `no -> 1904`.
263
 
264
  **CUDA runs out of memory**
 
1
+ # KaLM-Reranker-V1-Nano vLLM Support
2
 
3
  This directory contains the experimental vLLM 0.19.1 adapter for
4
+ `KaLM-Embedding/KaLM-Reranker-V1-Nano`. It supports offline Python and CLI
5
  reranking plus an optional FastAPI service.
6
 
7
  The adapter does not modify or retrain the checkpoint. It reads the original
 
35
  pip install "vllm==0.19.1" "transformers==5.6.2"
36
  pip install "fastapi>=0.136,<0.137" "uvicorn>=0.46,<0.47"
37
 
38
+ hf download KaLM-Embedding/KaLM-Reranker-V1-Nano \
39
+ --local-dir ./KaLM-Reranker-V1-Nano
40
+ pip install ./KaLM-Reranker-V1-Nano/vllm_support --no-deps
41
  export VLLM_PLUGINS=kalm_t5gemma2
42
  ```
43
 
 
45
  only download the `vllm_support` directory before installing the plugin:
46
 
47
  ```bash
48
+ hf download KaLM-Embedding/KaLM-Reranker-V1-Nano \
49
  --include "vllm_support/**" \
50
+ --local-dir ./KaLM-Reranker-V1-Nano
51
+ pip install ./KaLM-Reranker-V1-Nano/vllm_support --no-deps
52
  ```
53
 
54
  ## Offline Python API
 
64
 
65
  pairs = [(query, document) for document in documents]
66
  with KaLMVLLMReranker(
67
+ "KaLM-Embedding/KaLM-Reranker-V1-Nano",
68
  query_max_length=512,
69
  document_max_length=1024,
70
  encoder_chunk_size=4,
 
97
 
98
  ```bash
99
  kalm-vllm-rerank \
100
+ --input-jsonl ./KaLM-Reranker-V1-Nano/vllm_support/examples/sample_pairs.jsonl \
101
  --output-jsonl ./scores.jsonl \
102
  --return-margin
103
  ```
 
114
  kalm-vllm-serve \
115
  --host 0.0.0.0 \
116
  --port 8000 \
117
+ --model KaLM-Embedding/KaLM-Reranker-V1-Nano \
118
  --encoder-chunk-size 4
119
  ```
120
 
 
123
 
124
  ```bash
125
  CUDA_VISIBLE_DEVICES=0 PORT=8000 \
126
+ ./KaLM-Reranker-V1-Nano/vllm_support/examples/start_online_server.sh
127
  ```
128
 
129
  In a second terminal, check health and send built-in demo requests:
 
140
  ```bash
141
  kalm-vllm-client \
142
  --endpoint rerank \
143
+ --json-file ./KaLM-Reranker-V1-Nano/vllm_support/examples/rerank_request.json \
144
  --return-margin \
145
  --top-k 10
146
  ```
 
150
  ```bash
151
  kalm-vllm-client \
152
  --endpoint score \
153
+ --json-file ./KaLM-Reranker-V1-Nano/vllm_support/examples/score_request.json \
154
  --return-margin
155
  ```
156
 
 
247
  point name:
248
 
249
  ```bash
250
+ pip install ./KaLM-Reranker-V1-Nano/vllm_support --no-deps --force-reinstall
251
  export VLLM_PLUGINS=kalm_t5gemma2
252
  ```
253
 
 
258
 
259
  **The tokenizer check fails**
260
 
261
+ Confirm that the tokenizer belongs to this Nano checkpoint. The adapter
262
  requires `yes -> 4443` and `no -> 1904`.
263
 
264
  **CUDA runs out of memory**
vllm_support/build/lib/kalm_t5gemma2_vllm_plugin/__init__.py DELETED
@@ -1,23 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from .constants import (
4
- ARCHITECTURE,
5
- MODEL_ID,
6
- PLUGIN_NAME,
7
- SUPPORTED_ENCODER_CHUNK_SIZES,
8
- TEXT_MODALITY,
9
- )
10
- from .reranker import KaLMVLLMOfflineReranker, KaLMVLLMReranker
11
-
12
- __version__ = "0.1.0"
13
-
14
- __all__ = [
15
- "ARCHITECTURE",
16
- "KaLMVLLMOfflineReranker",
17
- "KaLMVLLMReranker",
18
- "MODEL_ID",
19
- "PLUGIN_NAME",
20
- "SUPPORTED_ENCODER_CHUNK_SIZES",
21
- "TEXT_MODALITY",
22
- "__version__",
23
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
vllm_support/build/lib/kalm_t5gemma2_vllm_plugin/cli.py DELETED
@@ -1,172 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
- import json
5
- import sys
6
- from collections import OrderedDict
7
- from pathlib import Path
8
- from typing import Any, Iterable, TextIO
9
-
10
- from .constants import (
11
- DEFAULT_DOCUMENT_MAX_LENGTH,
12
- DEFAULT_ENCODER_CHUNK_SIZE,
13
- DEFAULT_MAX_MODEL_LEN,
14
- DEFAULT_QUERY_MAX_LENGTH,
15
- MODEL_ID,
16
- SAMPLE_DOCUMENTS,
17
- SAMPLE_QUERY,
18
- SUPPORTED_ENCODER_CHUNK_SIZES,
19
- parse_encoder_chunk_size,
20
- )
21
- from .reranker import KaLMVLLMReranker
22
-
23
-
24
- def build_parser() -> argparse.ArgumentParser:
25
- parser = argparse.ArgumentParser(
26
- description="Offline KaLM-Reranker-V1-Nano scoring with vLLM 0.19.1.",
27
- formatter_class=argparse.ArgumentDefaultsHelpFormatter,
28
- )
29
- parser.add_argument("--input-jsonl", type=Path)
30
- parser.add_argument("--output-jsonl", type=Path)
31
- parser.add_argument("--model", default=MODEL_ID)
32
- parser.add_argument("--query-max-length", type=int, default=DEFAULT_QUERY_MAX_LENGTH)
33
- parser.add_argument(
34
- "--document-max-length", type=int, default=DEFAULT_DOCUMENT_MAX_LENGTH
35
- )
36
- parser.add_argument(
37
- "--encoder-chunk-size",
38
- default=str(DEFAULT_ENCODER_CHUNK_SIZE),
39
- help=f"One of {sorted(SUPPORTED_ENCODER_CHUNK_SIZES)}.",
40
- )
41
- parser.add_argument("--max-model-len", type=int, default=DEFAULT_MAX_MODEL_LEN)
42
- parser.add_argument("--batch-size", type=int, default=32)
43
- parser.add_argument("--return-margin", action="store_true")
44
- parser.add_argument("--top-k", type=int)
45
- parser.add_argument("--dtype", default="bfloat16")
46
- parser.add_argument("--gpu-memory-utilization", type=float, default=0.85)
47
- parser.add_argument("--tensor-parallel-size", type=int, default=1)
48
- return parser
49
-
50
-
51
- def _read_jsonl(path: Path) -> list[dict[str, Any]]:
52
- rows: list[dict[str, Any]] = []
53
- with path.open("r", encoding="utf-8") as handle:
54
- for line_no, line in enumerate(handle, start=1):
55
- if not line.strip():
56
- continue
57
- try:
58
- row = json.loads(line)
59
- except json.JSONDecodeError as error:
60
- raise ValueError(f"{path}:{line_no}: invalid JSON.") from error
61
- if not isinstance(row, dict):
62
- raise ValueError(f"{path}:{line_no}: expected a JSON object.")
63
- if not isinstance(row.get("query"), str):
64
- raise ValueError(f"{path}:{line_no}: 'query' must be a string.")
65
- if not isinstance(row.get("document"), str):
66
- raise ValueError(f"{path}:{line_no}: 'document' must be a string.")
67
- if "instruction" in row and not isinstance(row["instruction"], str):
68
- raise ValueError(f"{path}:{line_no}: 'instruction' must be a string.")
69
- rows.append(row)
70
- return rows
71
-
72
-
73
- def _write_jsonl(rows: Iterable[dict[str, Any]], output: TextIO) -> None:
74
- for row in rows:
75
- output.write(json.dumps(row, ensure_ascii=False) + "\n")
76
- output.flush()
77
-
78
-
79
- def _score_rows(
80
- reranker: KaLMVLLMReranker,
81
- rows: list[dict[str, Any]],
82
- *,
83
- return_margin: bool,
84
- top_k: int | None,
85
- ) -> list[dict[str, Any]]:
86
- grouped: OrderedDict[str | None, list[tuple[int, dict[str, Any]]]] = OrderedDict()
87
- for index, row in enumerate(rows):
88
- grouped.setdefault(row.get("instruction"), []).append((index, row))
89
-
90
- scored: dict[int, dict[str, Any]] = {}
91
- for instruction, items in grouped.items():
92
- predictions = reranker.predict(
93
- [(row["query"], row["document"]) for _, row in items],
94
- instruction=instruction,
95
- return_margin=True,
96
- )
97
- for (index, row), prediction in zip(items, predictions):
98
- assert isinstance(prediction, dict)
99
- result = {
100
- "id": row.get("id", index),
101
- "query": row["query"],
102
- "document": row["document"],
103
- "score": prediction["score"],
104
- }
105
- if "instruction" in row:
106
- result["instruction"] = row["instruction"]
107
- if return_margin:
108
- result["margin"] = prediction["margin"]
109
- scored[index] = result
110
-
111
- ordered = [scored[index] for index in range(len(rows))]
112
- if top_k is None:
113
- return ordered
114
- if top_k < 0:
115
- raise ValueError("--top-k must be non-negative.")
116
- by_query: OrderedDict[str, list[dict[str, Any]]] = OrderedDict()
117
- for row in ordered:
118
- by_query.setdefault(str(row["query"]), []).append(row)
119
- output: list[dict[str, Any]] = []
120
- for group in by_query.values():
121
- group.sort(key=lambda item: float(item["score"]), reverse=True)
122
- output.extend(group[:top_k])
123
- return output
124
-
125
-
126
- def main() -> int:
127
- args = build_parser().parse_args()
128
- with KaLMVLLMReranker(
129
- args.model,
130
- query_max_length=args.query_max_length,
131
- document_max_length=args.document_max_length,
132
- encoder_chunk_size=parse_encoder_chunk_size(args.encoder_chunk_size),
133
- max_model_len=args.max_model_len,
134
- batch_size=args.batch_size,
135
- dtype=args.dtype,
136
- gpu_memory_utilization=args.gpu_memory_utilization,
137
- tensor_parallel_size=args.tensor_parallel_size,
138
- ) as reranker:
139
- if args.input_jsonl is None:
140
- rankings = reranker.rank(
141
- SAMPLE_QUERY,
142
- SAMPLE_DOCUMENTS,
143
- top_k=args.top_k,
144
- return_margin=args.return_margin,
145
- )
146
- rows = [
147
- {
148
- "query": SAMPLE_QUERY,
149
- "document": SAMPLE_DOCUMENTS[int(item["corpus_id"])],
150
- **item,
151
- }
152
- for item in rankings
153
- ]
154
- else:
155
- rows = _score_rows(
156
- reranker,
157
- _read_jsonl(args.input_jsonl),
158
- return_margin=args.return_margin,
159
- top_k=args.top_k,
160
- )
161
-
162
- if args.output_jsonl is None:
163
- _write_jsonl(rows, sys.stdout)
164
- else:
165
- args.output_jsonl.parent.mkdir(parents=True, exist_ok=True)
166
- with args.output_jsonl.open("w", encoding="utf-8") as handle:
167
- _write_jsonl(rows, handle)
168
- return 0
169
-
170
-
171
- if __name__ == "__main__":
172
- raise SystemExit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
vllm_support/build/lib/kalm_t5gemma2_vllm_plugin/client.py DELETED
@@ -1,125 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
- import json
5
- import sys
6
- import urllib.error
7
- import urllib.request
8
- from pathlib import Path
9
- from typing import Any
10
-
11
- from .constants import SAMPLE_DOCUMENTS, SAMPLE_QUERY
12
-
13
-
14
- def build_parser() -> argparse.ArgumentParser:
15
- parser = argparse.ArgumentParser(
16
- description="Call the KaLM vLLM FastAPI service.",
17
- formatter_class=argparse.ArgumentDefaultsHelpFormatter,
18
- )
19
- parser.add_argument("--base-url", default="http://127.0.0.1:8000")
20
- parser.add_argument("--health", action="store_true")
21
- parser.add_argument("--endpoint", choices=("rerank", "score"), default="rerank")
22
- parser.add_argument("--json-file", type=Path)
23
- parser.add_argument("--return-margin", action="store_true")
24
- parser.add_argument("--top-k", type=int)
25
- parser.add_argument("--timeout", type=float, default=600.0)
26
- return parser
27
-
28
-
29
- def request_json(
30
- method: str,
31
- url: str,
32
- *,
33
- payload: dict[str, Any] | None = None,
34
- timeout: float = 600.0,
35
- ) -> dict[str, Any]:
36
- data = None
37
- headers = {"Accept": "application/json"}
38
- if payload is not None:
39
- data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
40
- headers["Content-Type"] = "application/json"
41
- request = urllib.request.Request(url, data=data, headers=headers, method=method)
42
- opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
43
- try:
44
- with opener.open(request, timeout=timeout) as response:
45
- raw = response.read().decode("utf-8")
46
- except urllib.error.HTTPError as error:
47
- detail = error.read().decode("utf-8", errors="replace")
48
- raise RuntimeError(f"HTTP {error.code} from {url}: {detail}") from error
49
- return json.loads(raw)
50
-
51
-
52
- def _demo_payload(
53
- endpoint: str,
54
- return_margin: bool,
55
- top_k: int | None,
56
- ) -> dict[str, Any]:
57
- if endpoint == "rerank":
58
- return {
59
- "query": SAMPLE_QUERY,
60
- "documents": list(SAMPLE_DOCUMENTS),
61
- "top_k": top_k,
62
- "return_margin": return_margin,
63
- }
64
- return {
65
- "pairs": [
66
- {
67
- "id": "positive",
68
- "query": SAMPLE_QUERY,
69
- "document": SAMPLE_DOCUMENTS[0],
70
- },
71
- {
72
- "id": "negative",
73
- "query": SAMPLE_QUERY,
74
- "document": SAMPLE_DOCUMENTS[1],
75
- },
76
- ],
77
- "return_margin": return_margin,
78
- }
79
-
80
-
81
- def _load_payload(path: Path) -> dict[str, Any]:
82
- with path.open("r", encoding="utf-8") as handle:
83
- payload = json.load(handle)
84
- if not isinstance(payload, dict):
85
- raise ValueError(f"{path} must contain one JSON object.")
86
- return payload
87
-
88
-
89
- def _request_payload(args: argparse.Namespace) -> dict[str, Any]:
90
- payload = (
91
- _load_payload(args.json_file)
92
- if args.json_file is not None
93
- else _demo_payload(args.endpoint, args.return_margin, args.top_k)
94
- )
95
- if args.return_margin:
96
- payload["return_margin"] = True
97
- if args.top_k is not None:
98
- if args.endpoint != "rerank":
99
- raise ValueError("--top-k is only valid with --endpoint rerank.")
100
- if args.top_k < 0:
101
- raise ValueError("--top-k must be non-negative.")
102
- payload["top_k"] = args.top_k
103
- return payload
104
-
105
-
106
- def main() -> int:
107
- args = build_parser().parse_args()
108
- base_url = args.base_url.rstrip("/")
109
- if args.health:
110
- response = request_json("GET", f"{base_url}/health", timeout=args.timeout)
111
- else:
112
- payload = _request_payload(args)
113
- response = request_json(
114
- "POST",
115
- f"{base_url}/{args.endpoint}",
116
- payload=payload,
117
- timeout=args.timeout,
118
- )
119
- json.dump(response, sys.stdout, ensure_ascii=False, indent=2)
120
- sys.stdout.write("\n")
121
- return 0
122
-
123
-
124
- if __name__ == "__main__":
125
- raise SystemExit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
vllm_support/build/lib/kalm_t5gemma2_vllm_plugin/constants.py DELETED
@@ -1,105 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Iterable
4
-
5
-
6
- MODEL_ID = "KaLM-Embedding/KaLM-Reranker-V1-Nano"
7
- PLUGIN_NAME = "kalm_t5gemma2"
8
- ARCHITECTURE = "T5Gemma2VllmScoreClassification"
9
- TEXT_MODALITY = "text"
10
-
11
- DEFAULT_INSTRUCTION = "Given a query, retrieve documents that answer the query."
12
- DEFAULT_SYSTEM_INSTRUCTION = (
13
- "Judge whether the Document meets the requirements based on the Query and "
14
- 'the Instruct provided. Note that the answer can only be "yes" or "no".'
15
- )
16
-
17
- DEFAULT_QUERY_MAX_LENGTH = 512
18
- DEFAULT_DOCUMENT_MAX_LENGTH = 1024
19
- DEFAULT_MAX_MODEL_LEN = 2048
20
- DEFAULT_ENCODER_CHUNK_SIZE = 4
21
- DEFAULT_DECODER_PAD_TO_MULTIPLE_OF = 8
22
- SUPPORTED_ENCODER_CHUNK_SIZES = frozenset({1, 2, 4, 8, 16, 32})
23
-
24
- YES_TOKEN_TEXT = "yes"
25
- NO_TOKEN_TEXT = "no"
26
- YES_TOKEN_ID = 4443
27
- NO_TOKEN_ID = 1904
28
-
29
- SAMPLE_QUERY = "What is the capital of China?"
30
- SAMPLE_DOCUMENTS = (
31
- "The capital of China is Beijing.",
32
- "Gravity attracts bodies toward one another.",
33
- )
34
-
35
-
36
- def parse_encoder_chunk_size(value: object) -> int:
37
- if value is None:
38
- return DEFAULT_ENCODER_CHUNK_SIZE
39
- if isinstance(value, str):
40
- normalized = value.strip().lower()
41
- if normalized in {"", "none", "null"}:
42
- return DEFAULT_ENCODER_CHUNK_SIZE
43
- try:
44
- parsed = int(normalized)
45
- except ValueError as error:
46
- raise ValueError(_chunk_size_error(value)) from error
47
- else:
48
- parsed = int(value)
49
- if parsed not in SUPPORTED_ENCODER_CHUNK_SIZES:
50
- raise ValueError(_chunk_size_error(value))
51
- return parsed
52
-
53
-
54
- def _chunk_size_error(value: object) -> str:
55
- return (
56
- "encoder_chunk_size must be one of "
57
- f"{sorted(SUPPORTED_ENCODER_CHUNK_SIZES)}; got {value!r}."
58
- )
59
-
60
-
61
- def encoder_text(document: str) -> str:
62
- return f"<Document>: {document}"
63
-
64
-
65
- def decoder_text(
66
- tokenizer,
67
- query: str,
68
- *,
69
- instruction: str = DEFAULT_INSTRUCTION,
70
- system_instruction: str = DEFAULT_SYSTEM_INSTRUCTION,
71
- query_max_length: int = DEFAULT_QUERY_MAX_LENGTH,
72
- ) -> str:
73
- query_ids = tokenizer(
74
- query,
75
- add_special_tokens=False,
76
- truncation=True,
77
- max_length=query_max_length,
78
- )["input_ids"]
79
- truncated_query = tokenizer.decode(
80
- query_ids,
81
- skip_special_tokens=False,
82
- clean_up_tokenization_spaces=False,
83
- )
84
- return (
85
- "<bos><start_of_turn>user\n"
86
- f"{system_instruction}\n\n"
87
- f"<Instruct>: {instruction}\n"
88
- f"<Query>: {truncated_query}<end_of_turn>\n"
89
- "<start_of_turn>model\n\n\n\n"
90
- )
91
-
92
-
93
- def validate_answer_tokens(tokenizer) -> None:
94
- expected: Iterable[tuple[str, int]] = (
95
- (YES_TOKEN_TEXT, YES_TOKEN_ID),
96
- (NO_TOKEN_TEXT, NO_TOKEN_ID),
97
- )
98
- for text, token_id in expected:
99
- actual = tokenizer.encode(text, add_special_tokens=False)
100
- if actual != [token_id]:
101
- raise RuntimeError(
102
- f"Unexpected tokenization for {text!r}: expected {[token_id]}, "
103
- f"got {actual}."
104
- )
105
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
vllm_support/build/lib/kalm_t5gemma2_vllm_plugin/modeling.py DELETED
@@ -1,265 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import math
4
- import os
5
- from typing import Iterable
6
-
7
- import torch
8
- import torch.nn as nn
9
- from transformers.modeling_outputs import BaseModelOutput
10
- from vllm.model_executor.layers.pooler import DispatchPooler
11
- from vllm.multimodal import MULTIMODAL_REGISTRY
12
-
13
- from .constants import (
14
- DEFAULT_DECODER_PAD_TO_MULTIPLE_OF,
15
- DEFAULT_ENCODER_CHUNK_SIZE,
16
- NO_TOKEN_ID,
17
- TEXT_MODALITY,
18
- YES_TOKEN_ID,
19
- )
20
- from .modeling_score import T5Gemma2ForScoreClassification
21
- from .processing import (
22
- TextEncoderDummyInputsBuilder,
23
- TextEncoderProcessingInfo,
24
- TextEncoderProcessor,
25
- )
26
-
27
-
28
- def _as_token_rows(value: object) -> list[torch.Tensor]:
29
- if isinstance(value, torch.Tensor):
30
- if value.ndim == 1:
31
- return [value]
32
- if value.ndim == 2:
33
- return [row for row in value]
34
- raise ValueError(f"encoder_input_ids must be 1D/2D, got {value.shape}.")
35
- if isinstance(value, list):
36
- return [
37
- item.flatten()
38
- if isinstance(item, torch.Tensor)
39
- else torch.tensor(item, dtype=torch.long)
40
- for item in value
41
- ]
42
- raise TypeError(f"Unsupported encoder_input_ids type: {type(value)!r}")
43
-
44
-
45
- def _split_by_position_zero(
46
- input_ids: torch.Tensor,
47
- positions: torch.Tensor,
48
- ) -> tuple[list[torch.Tensor], list[int]]:
49
- flat_ids = input_ids.flatten()
50
- starts = (positions.flatten() == 0).nonzero(as_tuple=False).flatten().tolist()
51
- if not starts:
52
- return [], []
53
- starts.append(int(flat_ids.numel()))
54
- rows: list[torch.Tensor] = []
55
- last_indices: list[int] = []
56
- for start, end in zip(starts[:-1], starts[1:]):
57
- if end > start:
58
- rows.append(flat_ids[start:end])
59
- last_indices.append(end - 1)
60
- return rows, last_indices
61
-
62
-
63
- def _debug(message: str) -> None:
64
- if os.environ.get("KALM_VLLM_DEBUG") == "1":
65
- print(f"[kalm-vllm-debug] {message}", flush=True)
66
-
67
-
68
- @MULTIMODAL_REGISTRY.register_processor(
69
- TextEncoderProcessor,
70
- info=TextEncoderProcessingInfo,
71
- dummy_inputs=TextEncoderDummyInputsBuilder,
72
- )
73
- class T5Gemma2VllmScoreClassification(nn.Module):
74
- is_pooling_model = True
75
- supports_multimodal = True
76
- score_type = "cross-encoder"
77
- attn_type = "encoder_decoder"
78
- default_seq_pooling_type = "LAST"
79
- default_tok_pooling_type = "ALL"
80
-
81
- def __init__(self, *, vllm_config, prefix: str = "") -> None:
82
- super().__init__()
83
- self.vllm_config = vllm_config
84
- self.model_config = vllm_config.model_config
85
- self.config = self.model_config.hf_config
86
- self.config.num_labels = 1
87
- self.config.yes_token_id = int(
88
- getattr(self.config, "yes_token_id", YES_TOKEN_ID)
89
- )
90
- self.config.no_token_id = int(
91
- getattr(self.config, "no_token_id", NO_TOKEN_ID)
92
- )
93
- self.config.encoder_chunk_size = getattr(
94
- self.config,
95
- "encoder_chunk_size",
96
- DEFAULT_ENCODER_CHUNK_SIZE,
97
- )
98
- self.encoder_chunk_size = self.config.encoder_chunk_size
99
- self.config.decoder_pad_to_multiple_of = int(
100
- getattr(
101
- self.config,
102
- "decoder_pad_to_multiple_of",
103
- DEFAULT_DECODER_PAD_TO_MULTIPLE_OF,
104
- )
105
- )
106
- self.decoder_pad_to_multiple_of = self.config.decoder_pad_to_multiple_of
107
- self.pad_token_id = int(getattr(self.config, "pad_token_id", 0) or 0)
108
-
109
- self.score_model = T5Gemma2ForScoreClassification.from_pretrained(
110
- self.model_config.model,
111
- trust_remote_code=True,
112
- dtype=self.model_config.dtype,
113
- )
114
- self.score_model.config.yes_token_id = self.config.yes_token_id
115
- self.score_model.config.no_token_id = self.config.no_token_id
116
- self.score_model.config.num_labels = 1
117
- self.score_model.config.encoder_chunk_size = self.encoder_chunk_size
118
- self.score_model.yes_token_id = self.config.yes_token_id
119
- self.score_model.no_token_id = self.config.no_token_id
120
- self.score_model.encoder_chunk_size = self.encoder_chunk_size
121
- self.score_model._validate_score_config()
122
- self.score_model.eval()
123
-
124
- pooler_config = self.model_config.pooler_config
125
- assert pooler_config is not None
126
- self.pooler = DispatchPooler.for_seq_cls(pooler_config)
127
-
128
- def get_language_model(self):
129
- return self
130
-
131
- def get_num_mm_encoder_tokens(self, num_tokens: int) -> int:
132
- if self.encoder_chunk_size is None:
133
- return int(num_tokens)
134
- return int(math.ceil(num_tokens / int(self.encoder_chunk_size)))
135
-
136
- def embed_input_ids(self, input_ids: torch.Tensor, *args, **kwargs) -> torch.Tensor:
137
- return self.score_model.get_input_embeddings()(input_ids)
138
-
139
- def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
140
- # The semantic wrapper loads the same checkpoint directly.
141
- for _ in weights:
142
- pass
143
- return set(self.state_dict().keys())
144
-
145
- def embed_multimodal(self, **kwargs: object) -> list[torch.Tensor]:
146
- if "encoder_input_ids" not in kwargs:
147
- raise ValueError(f"Missing {TEXT_MODALITY} encoder_input_ids.")
148
- rows = _as_token_rows(kwargs["encoder_input_ids"])
149
- device = next(self.score_model.parameters()).device
150
- outputs: list[torch.Tensor] = []
151
- with torch.inference_mode():
152
- for row in rows:
153
- input_ids = row.to(device=device, dtype=torch.long).unsqueeze(0)
154
- attention_mask = torch.ones_like(input_ids)
155
- raw_encoder_outputs = self.score_model.get_encoder()(
156
- input_ids=input_ids,
157
- attention_mask=attention_mask,
158
- return_dict=True,
159
- )
160
- hidden = raw_encoder_outputs.last_hidden_state
161
- if self.encoder_chunk_size is not None:
162
- hidden, _ = self.score_model._pool_encoder_chunks(
163
- hidden,
164
- attention_mask,
165
- int(self.encoder_chunk_size),
166
- )
167
- item = hidden.squeeze(0).contiguous()
168
- _debug(f"encoder output shape={tuple(item.shape)}")
169
- outputs.append(item)
170
- return outputs
171
-
172
- def _pad_decoder_rows(
173
- self,
174
- rows: list[torch.Tensor],
175
- device: torch.device,
176
- ) -> tuple[torch.Tensor, torch.Tensor]:
177
- max_len = max(int(row.numel()) for row in rows)
178
- if self.decoder_pad_to_multiple_of > 1:
179
- multiple = self.decoder_pad_to_multiple_of
180
- max_len = int(math.ceil(max_len / multiple) * multiple)
181
- batch = torch.full(
182
- (len(rows), max_len),
183
- self.pad_token_id,
184
- dtype=torch.long,
185
- device=device,
186
- )
187
- mask = torch.zeros_like(batch)
188
- for index, row in enumerate(rows):
189
- row = row.to(device=device, dtype=torch.long)
190
- length = int(row.numel())
191
- batch[index, :length] = row
192
- mask[index, :length] = 1
193
- return batch, mask
194
-
195
- @staticmethod
196
- def _pad_encoder_outputs(
197
- encoder_outputs: list[torch.Tensor],
198
- device: torch.device,
199
- ) -> tuple[torch.Tensor, torch.Tensor]:
200
- max_len = max(int(item.shape[0]) for item in encoder_outputs)
201
- hidden_size = int(encoder_outputs[0].shape[-1])
202
- batch = torch.zeros(
203
- (len(encoder_outputs), max_len, hidden_size),
204
- dtype=encoder_outputs[0].dtype,
205
- device=device,
206
- )
207
- mask = torch.zeros(
208
- (len(encoder_outputs), max_len),
209
- dtype=torch.long,
210
- device=device,
211
- )
212
- for index, item in enumerate(encoder_outputs):
213
- item = item.to(device=device)
214
- length = int(item.shape[0])
215
- batch[index, :length] = item
216
- mask[index, :length] = 1
217
- return batch, mask
218
-
219
- def forward(
220
- self,
221
- input_ids: torch.Tensor | None,
222
- positions: torch.Tensor,
223
- intermediate_tensors=None,
224
- inputs_embeds: torch.Tensor | None = None,
225
- encoder_outputs: list[torch.Tensor] | torch.Tensor | None = None,
226
- **kwargs,
227
- ) -> torch.Tensor:
228
- if input_ids is None:
229
- raise ValueError("Decoder input_ids are required.")
230
- decoder_rows, last_indices = _split_by_position_zero(input_ids, positions)
231
- hidden = input_ids.new_zeros((input_ids.numel(), 1), dtype=torch.float32)
232
- if not decoder_rows:
233
- return hidden
234
- if encoder_outputs is None:
235
- return hidden
236
-
237
- encoder_list = (
238
- [item for item in encoder_outputs]
239
- if isinstance(encoder_outputs, torch.Tensor)
240
- else list(encoder_outputs)
241
- )
242
- if len(encoder_list) != len(decoder_rows):
243
- raise ValueError(
244
- "Mismatched encoder/decoder batch sizes: "
245
- f"{len(encoder_list)} vs {len(decoder_rows)}."
246
- )
247
-
248
- device = next(self.score_model.parameters()).device
249
- decoder_batch, decoder_mask = self._pad_decoder_rows(decoder_rows, device)
250
- encoder_batch, encoder_mask = self._pad_encoder_outputs(encoder_list, device)
251
- with torch.inference_mode():
252
- outputs = self.score_model(
253
- encoder_outputs=BaseModelOutput(last_hidden_state=encoder_batch),
254
- attention_mask=encoder_mask,
255
- decoder_input_ids=decoder_batch,
256
- decoder_attention_mask=decoder_mask,
257
- )
258
- margins = outputs.logits.squeeze(-1).to(hidden.device, dtype=hidden.dtype)
259
- _debug(f"margins={margins.float().tolist()}")
260
- for row_index, last_index in enumerate(last_indices):
261
- hidden[last_index, 0] = margins[row_index]
262
- return hidden
263
-
264
-
265
- __all__ = ["T5Gemma2VllmScoreClassification"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
vllm_support/build/lib/kalm_t5gemma2_vllm_plugin/modeling_score.py DELETED
@@ -1,189 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Optional, Tuple
4
-
5
- import torch
6
- import torch.nn.functional as F
7
- from transformers.modeling_outputs import BaseModelOutput, SequenceClassifierOutput
8
- from transformers.models.t5gemma2.modeling_t5gemma2 import (
9
- T5Gemma2ForConditionalGeneration,
10
- )
11
-
12
- from .constants import (
13
- DEFAULT_ENCODER_CHUNK_SIZE,
14
- NO_TOKEN_ID,
15
- YES_TOKEN_ID,
16
- )
17
-
18
-
19
- class T5Gemma2ForScoreClassification(T5Gemma2ForConditionalGeneration):
20
- """Expose the pretrained yes/no decision as one classification logit."""
21
-
22
- def __init__(self, config):
23
- super().__init__(config)
24
- self.num_labels = 1
25
- self.yes_token_id = int(getattr(config, "yes_token_id", YES_TOKEN_ID))
26
- self.no_token_id = int(getattr(config, "no_token_id", NO_TOKEN_ID))
27
- self.encoder_chunk_size = getattr(
28
- config, "encoder_chunk_size", DEFAULT_ENCODER_CHUNK_SIZE
29
- )
30
- if self.encoder_chunk_size is not None:
31
- self.encoder_chunk_size = int(self.encoder_chunk_size)
32
- self._validate_score_config()
33
-
34
- def _validate_score_config(self) -> None:
35
- vocab_size = int(getattr(self.config, "vocab_size", 0))
36
- if vocab_size <= 0:
37
- raise ValueError("config.vocab_size must be a positive integer.")
38
- for name, token_id in (
39
- ("yes_token_id", self.yes_token_id),
40
- ("no_token_id", self.no_token_id),
41
- ):
42
- if token_id < 0 or token_id >= vocab_size:
43
- raise ValueError(
44
- f"{name}={token_id} is outside vocab_size={vocab_size}."
45
- )
46
- if self.encoder_chunk_size is not None and self.encoder_chunk_size <= 0:
47
- raise ValueError("encoder_chunk_size must be positive or None.")
48
-
49
- self.config.num_labels = 1
50
- self.config.yes_token_id = self.yes_token_id
51
- self.config.no_token_id = self.no_token_id
52
- self.config.encoder_chunk_size = self.encoder_chunk_size
53
-
54
- @staticmethod
55
- def _pool_encoder_chunks(
56
- hidden_states: torch.Tensor,
57
- attention_mask: torch.Tensor,
58
- chunk_size: int,
59
- ) -> Tuple[torch.Tensor, torch.Tensor]:
60
- batch_size, sequence_length, hidden_size = hidden_states.shape
61
- num_chunks = (sequence_length + chunk_size - 1) // chunk_size
62
- padded_length = num_chunks * chunk_size
63
- pad_length = padded_length - sequence_length
64
- if pad_length:
65
- hidden_states = F.pad(hidden_states, (0, 0, 0, pad_length))
66
- attention_mask = F.pad(attention_mask, (0, pad_length))
67
-
68
- hidden_states = hidden_states.view(
69
- batch_size, num_chunks, chunk_size, hidden_size
70
- )
71
- chunk_mask = attention_mask.view(batch_size, num_chunks, chunk_size)
72
- expanded_mask = chunk_mask.unsqueeze(-1).to(hidden_states.dtype)
73
- pooled_hidden = (hidden_states * expanded_mask).sum(dim=2)
74
- pooled_hidden /= chunk_mask.sum(dim=2).clamp(min=1).unsqueeze(-1)
75
- pooled_mask = (chunk_mask.sum(dim=2) > 0).to(attention_mask.dtype)
76
- return pooled_hidden, pooled_mask
77
-
78
- def _forward_with_optional_chunk_pooling(
79
- self,
80
- *,
81
- input_ids: Optional[torch.Tensor],
82
- attention_mask: Optional[torch.Tensor],
83
- decoder_input_ids: Optional[torch.Tensor],
84
- decoder_attention_mask: Optional[torch.Tensor],
85
- encoder_outputs=None,
86
- **kwargs,
87
- ):
88
- if self.encoder_chunk_size is None or encoder_outputs is not None:
89
- return super().forward(
90
- input_ids=input_ids,
91
- attention_mask=attention_mask,
92
- decoder_input_ids=decoder_input_ids,
93
- decoder_attention_mask=decoder_attention_mask,
94
- encoder_outputs=encoder_outputs,
95
- return_dict=True,
96
- **kwargs,
97
- )
98
- if input_ids is None:
99
- raise ValueError("input_ids are required when encoder_outputs is None.")
100
- if attention_mask is None:
101
- attention_mask = torch.ones_like(input_ids)
102
-
103
- raw_encoder_outputs = self.get_encoder()(
104
- input_ids=input_ids,
105
- attention_mask=attention_mask,
106
- return_dict=True,
107
- )
108
- pooled_hidden, pooled_mask = self._pool_encoder_chunks(
109
- raw_encoder_outputs.last_hidden_state,
110
- attention_mask,
111
- self.encoder_chunk_size,
112
- )
113
- return super().forward(
114
- encoder_outputs=BaseModelOutput(last_hidden_state=pooled_hidden),
115
- attention_mask=pooled_mask,
116
- decoder_input_ids=decoder_input_ids,
117
- decoder_attention_mask=decoder_attention_mask,
118
- return_dict=True,
119
- **kwargs,
120
- )
121
-
122
- def forward(
123
- self,
124
- input_ids: Optional[torch.Tensor] = None,
125
- attention_mask: Optional[torch.Tensor] = None,
126
- decoder_input_ids: Optional[torch.Tensor] = None,
127
- decoder_attention_mask: Optional[torch.Tensor] = None,
128
- encoder_outputs=None,
129
- labels: Optional[torch.Tensor] = None,
130
- output_attentions: Optional[bool] = None,
131
- output_hidden_states: Optional[bool] = None,
132
- return_dict: Optional[bool] = None,
133
- **kwargs,
134
- ) -> SequenceClassifierOutput:
135
- if labels is not None:
136
- raise NotImplementedError("This wrapper is inference-only.")
137
- if decoder_input_ids is None:
138
- raise ValueError("decoder_input_ids are required.")
139
- if decoder_input_ids.shape[0] == 0:
140
- raise ValueError("empty batches are not supported.")
141
- if input_ids is not None and input_ids.shape[0] == 0:
142
- raise ValueError("empty batches are not supported.")
143
-
144
- outputs = self._forward_with_optional_chunk_pooling(
145
- input_ids=input_ids,
146
- attention_mask=attention_mask,
147
- decoder_input_ids=decoder_input_ids,
148
- decoder_attention_mask=decoder_attention_mask,
149
- encoder_outputs=encoder_outputs,
150
- output_attentions=output_attentions,
151
- output_hidden_states=output_hidden_states,
152
- **kwargs,
153
- )
154
- logits = outputs.logits
155
- if decoder_attention_mask is None:
156
- sequence_lengths = torch.full(
157
- (logits.shape[0],),
158
- logits.shape[1] - 1,
159
- dtype=torch.long,
160
- device=logits.device,
161
- )
162
- else:
163
- sequence_lengths = decoder_attention_mask.sum(dim=1).to(torch.long) - 1
164
- if (sequence_lengths < 0).any():
165
- raise ValueError("decoder_attention_mask contains an empty sequence.")
166
-
167
- batch_indices = torch.arange(logits.shape[0], device=logits.device)
168
- last_logits = logits[batch_indices, sequence_lengths]
169
- yes_no_logits = torch.stack(
170
- (
171
- last_logits[:, self.yes_token_id],
172
- last_logits[:, self.no_token_id],
173
- ),
174
- dim=-1,
175
- ).float()
176
- if not torch.isfinite(yes_no_logits).all():
177
- bad_count = (~torch.isfinite(yes_no_logits).all(dim=-1)).sum().item()
178
- raise RuntimeError(f"Non-finite yes/no logits for {bad_count} input(s).")
179
-
180
- margin = yes_no_logits[:, 0] - yes_no_logits[:, 1]
181
- return SequenceClassifierOutput(
182
- logits=margin[:, None],
183
- hidden_states=getattr(outputs, "hidden_states", None),
184
- attentions=getattr(outputs, "attentions", None),
185
- )
186
-
187
-
188
- __all__ = ["T5Gemma2ForScoreClassification"]
189
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
vllm_support/build/lib/kalm_t5gemma2_vllm_plugin/processing.py DELETED
@@ -1,173 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from collections.abc import Mapping
4
- import hashlib
5
- from typing import Any
6
-
7
- import torch
8
- from transformers.feature_extraction_utils import BatchFeature
9
- from vllm.inputs import MultiModalDataDict, mm_enc_dec_input, mm_input
10
- from vllm.multimodal.inputs import MultiModalFieldConfig, PlaceholderRange
11
- from vllm.multimodal.parse import ModalityDataItems, MultiModalDataItems
12
- from vllm.multimodal.processing import (
13
- BaseDummyInputsBuilder,
14
- BaseProcessingInfo,
15
- EncDecMultiModalProcessor,
16
- ProcessorInputs,
17
- TimingContext,
18
- )
19
-
20
- from .constants import TEXT_MODALITY
21
-
22
-
23
- class TextTokenItems(ModalityDataItems[list[str], str]):
24
- def __init__(self, data: list[str]) -> None:
25
- super().__init__(data, TEXT_MODALITY)
26
-
27
- def get_count(self) -> int:
28
- return len(self.data)
29
-
30
- def get(self, index: int) -> str:
31
- return self.data[index]
32
-
33
- def get_processor_data(self) -> Mapping[str, object]:
34
- return {}
35
-
36
- def get_passthrough_data(self) -> Mapping[str, object]:
37
- return {}
38
-
39
-
40
- class TextEncoderProcessingInfo(BaseProcessingInfo):
41
- def get_supported_mm_limits(self) -> Mapping[str, int | None]:
42
- return {TEXT_MODALITY: 1}
43
-
44
- def get_mm_max_tokens_per_item(
45
- self,
46
- seq_len: int,
47
- mm_counts: Mapping[str, int],
48
- ) -> Mapping[str, int] | None:
49
- return {TEXT_MODALITY: seq_len}
50
-
51
- def parse_mm_data(
52
- self,
53
- mm_data: MultiModalDataDict,
54
- *,
55
- validate: bool = True,
56
- ) -> MultiModalDataItems:
57
- text_data = mm_data.get(TEXT_MODALITY)
58
- if text_data is None:
59
- items = TextTokenItems([])
60
- elif isinstance(text_data, str):
61
- items = TextTokenItems([text_data])
62
- elif isinstance(text_data, list):
63
- items = TextTokenItems([str(item) for item in text_data])
64
- else:
65
- items = TextTokenItems([str(text_data)])
66
- if validate:
67
- self.validate_num_items(TEXT_MODALITY, items.get_count())
68
- return MultiModalDataItems({TEXT_MODALITY: items})
69
-
70
-
71
- class TextEncoderDummyInputsBuilder(
72
- BaseDummyInputsBuilder[TextEncoderProcessingInfo]
73
- ):
74
- def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
75
- return "<Document>: dummy"
76
-
77
- def get_dummy_mm_data(
78
- self,
79
- seq_len: int,
80
- mm_counts: Mapping[str, int],
81
- mm_options: Mapping[str, Any],
82
- ) -> MultiModalDataDict:
83
- count = mm_counts.get(TEXT_MODALITY, 0)
84
- return {TEXT_MODALITY: ["<Document>: dummy"] * count}
85
-
86
-
87
- class TextEncoderProcessor(EncDecMultiModalProcessor[TextEncoderProcessingInfo]):
88
- skip_decoder_start_token = True
89
-
90
- def create_encoder_prompt(
91
- self,
92
- prompt: str | list[int],
93
- mm_items: MultiModalDataItems,
94
- ) -> str | list[int]:
95
- return prompt
96
-
97
- def _get_mm_fields_config(
98
- self,
99
- hf_inputs: BatchFeature,
100
- hf_processor_mm_kwargs: Mapping[str, object],
101
- ) -> Mapping[str, MultiModalFieldConfig]:
102
- return {
103
- "encoder_input_ids": MultiModalFieldConfig.batched(
104
- TEXT_MODALITY,
105
- keep_on_cpu=True,
106
- )
107
- }
108
-
109
- def _get_prompt_updates(self, *args, **kwargs):
110
- return []
111
-
112
- def apply(
113
- self,
114
- inputs: ProcessorInputs,
115
- timing_ctx: TimingContext,
116
- ):
117
- tokenizer = self.info.get_tokenizer()
118
- if isinstance(inputs.prompt, str):
119
- encoder_ids = tokenizer.encode(inputs.prompt, add_special_tokens=False)
120
- encoder_prompt_text = inputs.prompt
121
- else:
122
- encoder_ids = list(inputs.prompt)
123
- encoder_prompt_text = None
124
- if not encoder_ids:
125
- raise ValueError("The text encoder prompt cannot be empty.")
126
-
127
- tensor = torch.tensor([encoder_ids], dtype=torch.long)
128
- mm_kwargs = self._build_mm_kwargs(tensor)
129
- text_items = inputs.mm_data_items.get(TEXT_MODALITY)
130
- text_for_hash = (
131
- text_items.get(0)
132
- if text_items is not None and text_items.get_count() > 0
133
- else repr(encoder_ids)
134
- )
135
- digest = hashlib.sha256(str(text_for_hash).encode("utf-8")).hexdigest()
136
- mm_hashes = {TEXT_MODALITY: [f"{TEXT_MODALITY}:{digest}"]}
137
- mm_placeholders = {
138
- TEXT_MODALITY: [PlaceholderRange(offset=0, length=len(encoder_ids))]
139
- }
140
- encoder_inputs = mm_input(
141
- prompt_token_ids=encoder_ids,
142
- prompt=encoder_prompt_text,
143
- mm_kwargs=mm_kwargs,
144
- mm_hashes=mm_hashes,
145
- mm_placeholders=mm_placeholders,
146
- )
147
- return mm_enc_dec_input(
148
- encoder_inputs,
149
- decoder_prompt_token_ids=encoder_ids,
150
- decoder_prompt=encoder_prompt_text,
151
- )
152
-
153
- def _build_mm_kwargs(self, encoder_input_ids: torch.Tensor):
154
- return self._kwargs_from_batch_feature(
155
- BatchFeature({"encoder_input_ids": encoder_input_ids})
156
- )
157
-
158
- def _kwargs_from_batch_feature(self, batch: BatchFeature):
159
- from vllm.multimodal.inputs import MultiModalKwargsItems
160
-
161
- return MultiModalKwargsItems.from_hf_inputs(
162
- batch,
163
- self._get_mm_fields_config(batch, {}),
164
- )
165
-
166
-
167
- __all__ = [
168
- "TextEncoderDummyInputsBuilder",
169
- "TextEncoderProcessingInfo",
170
- "TextEncoderProcessor",
171
- "TextTokenItems",
172
- ]
173
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
vllm_support/build/lib/kalm_t5gemma2_vllm_plugin/register.py DELETED
@@ -1,15 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from vllm import ModelRegistry
4
-
5
- from .constants import ARCHITECTURE
6
-
7
-
8
- def register() -> None:
9
- ModelRegistry.register_model(
10
- ARCHITECTURE,
11
- "kalm_t5gemma2_vllm_plugin.modeling:T5Gemma2VllmScoreClassification",
12
- )
13
-
14
-
15
- __all__ = ["register"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
vllm_support/build/lib/kalm_t5gemma2_vllm_plugin/reranker.py DELETED
@@ -1,357 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import importlib.metadata
4
- import math
5
- import os
6
- from pathlib import Path
7
- from typing import Any, Iterable, Optional, Sequence
8
-
9
- from .constants import (
10
- ARCHITECTURE,
11
- DEFAULT_DECODER_PAD_TO_MULTIPLE_OF,
12
- DEFAULT_DOCUMENT_MAX_LENGTH,
13
- DEFAULT_ENCODER_CHUNK_SIZE,
14
- DEFAULT_INSTRUCTION,
15
- DEFAULT_MAX_MODEL_LEN,
16
- DEFAULT_QUERY_MAX_LENGTH,
17
- DEFAULT_SYSTEM_INSTRUCTION,
18
- MODEL_ID,
19
- NO_TOKEN_ID,
20
- PLUGIN_NAME,
21
- SUPPORTED_ENCODER_CHUNK_SIZES,
22
- TEXT_MODALITY,
23
- YES_TOKEN_ID,
24
- decoder_text,
25
- encoder_text,
26
- parse_encoder_chunk_size,
27
- validate_answer_tokens,
28
- )
29
-
30
-
31
- TESTED_VLLM_VERSION = "0.19.1"
32
-
33
-
34
- def _positive_int(value: int, name: str) -> int:
35
- parsed = int(value)
36
- if parsed <= 0:
37
- raise ValueError(f"{name} must be a positive integer.")
38
- return parsed
39
-
40
-
41
- def _sigmoid(value: float) -> float:
42
- if value >= 0:
43
- scale = math.exp(-value)
44
- return 1.0 / (1.0 + scale)
45
- scale = math.exp(value)
46
- return scale / (1.0 + scale)
47
-
48
-
49
- def build_hf_overrides(encoder_chunk_size: int) -> dict[str, object]:
50
- return {
51
- "architectures": [ARCHITECTURE],
52
- "num_labels": 1,
53
- "yes_token_id": YES_TOKEN_ID,
54
- "no_token_id": NO_TOKEN_ID,
55
- "encoder_chunk_size": encoder_chunk_size,
56
- "decoder_pad_to_multiple_of": DEFAULT_DECODER_PAD_TO_MULTIPLE_OF,
57
- "problem_type": "regression",
58
- }
59
-
60
-
61
- def _enable_plugin() -> None:
62
- allowed = os.environ.get("VLLM_PLUGINS")
63
- if allowed is None:
64
- os.environ["VLLM_PLUGINS"] = PLUGIN_NAME
65
- return
66
- names = {item.strip() for item in allowed.split(",") if item.strip()}
67
- if PLUGIN_NAME not in names:
68
- raise RuntimeError(
69
- f"VLLM_PLUGINS={allowed!r} excludes {PLUGIN_NAME!r}. Add the plugin "
70
- "name or unset VLLM_PLUGINS."
71
- )
72
-
73
-
74
- def check_runtime() -> None:
75
- installed_vllm = importlib.metadata.version("vllm")
76
- if installed_vllm != TESTED_VLLM_VERSION:
77
- raise RuntimeError(
78
- f"This adapter requires vLLM {TESTED_VLLM_VERSION}; "
79
- f"found {installed_vllm}."
80
- )
81
-
82
- discovered = {
83
- entry.name: entry.value
84
- for entry in importlib.metadata.entry_points(group="vllm.general_plugins")
85
- }
86
- if PLUGIN_NAME not in discovered:
87
- raise RuntimeError(
88
- f"vLLM plugin {PLUGIN_NAME!r} is not installed. Install the "
89
- "vllm_support package before creating the reranker."
90
- )
91
-
92
- from vllm.model_executor.models import ModelRegistry
93
- from vllm.plugins import load_general_plugins
94
-
95
- load_general_plugins()
96
- if ARCHITECTURE not in set(ModelRegistry.get_supported_archs()):
97
- raise RuntimeError(f"{ARCHITECTURE} was not registered in ModelRegistry.")
98
-
99
-
100
- class KaLMVLLMReranker:
101
- """Single-GPU vLLM adapter preserving the original KaLM score contract."""
102
-
103
- def __init__(
104
- self,
105
- model: str | Path = MODEL_ID,
106
- *,
107
- query_max_length: int = DEFAULT_QUERY_MAX_LENGTH,
108
- document_max_length: int = DEFAULT_DOCUMENT_MAX_LENGTH,
109
- encoder_chunk_size: object = DEFAULT_ENCODER_CHUNK_SIZE,
110
- dtype: str = "bfloat16",
111
- tensor_parallel_size: int = 1,
112
- max_model_len: int = DEFAULT_MAX_MODEL_LEN,
113
- gpu_memory_utilization: float = 0.85,
114
- batch_size: int = 32,
115
- instruction: str = DEFAULT_INSTRUCTION,
116
- system_instruction: str = DEFAULT_SYSTEM_INSTRUCTION,
117
- skip_runtime_check: bool = False,
118
- ) -> None:
119
- self.model = str(model)
120
- self.query_max_length = _positive_int(
121
- query_max_length, "query_max_length"
122
- )
123
- self.document_max_length = _positive_int(
124
- document_max_length, "document_max_length"
125
- )
126
- self.encoder_chunk_size = parse_encoder_chunk_size(encoder_chunk_size)
127
- self.dtype = str(dtype)
128
- self.tensor_parallel_size = _positive_int(
129
- tensor_parallel_size, "tensor_parallel_size"
130
- )
131
- if self.tensor_parallel_size != 1:
132
- raise ValueError(
133
- "The published adapter supports tensor_parallel_size=1 only."
134
- )
135
- self.max_model_len = _positive_int(max_model_len, "max_model_len")
136
- self.gpu_memory_utilization = float(gpu_memory_utilization)
137
- if not 0 < self.gpu_memory_utilization <= 1:
138
- raise ValueError("gpu_memory_utilization must be in the interval (0, 1].")
139
- self.batch_size = _positive_int(batch_size, "batch_size")
140
- if not isinstance(instruction, str) or not isinstance(
141
- system_instruction, str
142
- ):
143
- raise TypeError("instruction and system_instruction must be strings.")
144
- self.instruction = instruction
145
- self.system_instruction = system_instruction
146
-
147
- _enable_plugin()
148
- if not skip_runtime_check:
149
- check_runtime()
150
-
151
- from transformers import AutoTokenizer
152
-
153
- self.tokenizer = AutoTokenizer.from_pretrained(
154
- self.model,
155
- trust_remote_code=True,
156
- )
157
- validate_answer_tokens(self.tokenizer)
158
-
159
- from vllm import LLM
160
- from vllm.pooling_params import PoolingParams
161
-
162
- self.pooling_params = PoolingParams(use_activation=False)
163
- self.llm = LLM(
164
- model=self.model,
165
- runner="pooling",
166
- trust_remote_code=True,
167
- hf_overrides=build_hf_overrides(self.encoder_chunk_size),
168
- dtype=self.dtype,
169
- tensor_parallel_size=1,
170
- max_model_len=self.max_model_len,
171
- gpu_memory_utilization=self.gpu_memory_utilization,
172
- enforce_eager=True,
173
- limit_mm_per_prompt={TEXT_MODALITY: 1},
174
- )
175
-
176
- def close(self) -> None:
177
- llm = getattr(self, "llm", None)
178
- if llm is None:
179
- return
180
- engine = getattr(llm, "llm_engine", None)
181
- engine_core = getattr(engine, "engine_core", None)
182
- shutdown = getattr(engine_core, "shutdown", None)
183
- if callable(shutdown):
184
- shutdown()
185
- self.llm = None
186
-
187
- def __enter__(self) -> "KaLMVLLMReranker":
188
- return self
189
-
190
- def __exit__(self, exc_type, exc_value, traceback) -> None:
191
- self.close()
192
-
193
- def _encoder_ids(self, document: str) -> tuple[str, list[int]]:
194
- text = encoder_text(document)
195
- token_ids = self.tokenizer(
196
- text,
197
- add_special_tokens=False,
198
- truncation=True,
199
- max_length=self.document_max_length,
200
- )["input_ids"]
201
- if not token_ids:
202
- raise ValueError("Encoded document prompt is empty.")
203
- return text, list(token_ids)
204
-
205
- def _decoder_ids(self, query: str, instruction: str) -> tuple[str, list[int]]:
206
- text = decoder_text(
207
- self.tokenizer,
208
- query,
209
- instruction=instruction,
210
- system_instruction=self.system_instruction,
211
- query_max_length=self.query_max_length,
212
- )
213
- token_ids = self.tokenizer.encode(text, add_special_tokens=False)
214
- if not token_ids:
215
- raise ValueError("Encoded decoder prompt is empty.")
216
- return text, list(token_ids)
217
-
218
- def _prompt(self, query: str, document: str, instruction: str):
219
- from vllm.inputs import ExplicitEncoderDecoderPrompt, TokensPrompt
220
-
221
- encoder_prompt, encoder_ids = self._encoder_ids(document)
222
- decoder_prompt, decoder_ids = self._decoder_ids(query, instruction)
223
- return ExplicitEncoderDecoderPrompt(
224
- encoder_prompt=TokensPrompt(
225
- prompt_token_ids=encoder_ids,
226
- prompt=encoder_prompt,
227
- multi_modal_data={TEXT_MODALITY: [encoder_prompt]},
228
- ),
229
- decoder_prompt=TokensPrompt(
230
- prompt_token_ids=decoder_ids,
231
- prompt=decoder_prompt,
232
- ),
233
- )
234
-
235
- @staticmethod
236
- def _validate_pairs(
237
- pairs: Sequence[tuple[str, str]],
238
- ) -> list[tuple[str, str]]:
239
- if isinstance(pairs, (str, bytes)) or not isinstance(pairs, Sequence):
240
- raise TypeError("pairs must be a sequence of (query, document) pairs.")
241
- validated: list[tuple[str, str]] = []
242
- for index, pair in enumerate(pairs):
243
- if (
244
- isinstance(pair, (str, bytes))
245
- or not isinstance(pair, Sequence)
246
- or len(pair) != 2
247
- ):
248
- raise ValueError(f"pairs[{index}] must contain exactly two strings.")
249
- query, document = pair
250
- if not isinstance(query, str) or not isinstance(document, str):
251
- raise TypeError(f"pairs[{index}] must contain exactly two strings.")
252
- validated.append((query, document))
253
- return validated
254
-
255
- @staticmethod
256
- def _margins_from_outputs(outputs: Iterable[Any]) -> list[float]:
257
- margins: list[float] = []
258
- for output in outputs:
259
- values = output.outputs.probs
260
- if len(values) != 1:
261
- raise RuntimeError(f"Expected one raw margin, got {values}.")
262
- margin = float(values[0])
263
- if not math.isfinite(margin):
264
- raise RuntimeError(f"vLLM returned a non-finite margin: {margin}.")
265
- margins.append(margin)
266
- return margins
267
-
268
- def predict(
269
- self,
270
- pairs: Sequence[tuple[str, str]],
271
- *,
272
- instruction: Optional[str] = None,
273
- return_margin: bool = False,
274
- ) -> list[float] | list[dict[str, float]]:
275
- validated_pairs = self._validate_pairs(pairs)
276
- if not validated_pairs:
277
- return []
278
- effective_instruction = self.instruction if instruction is None else instruction
279
- if not isinstance(effective_instruction, str):
280
- raise TypeError("instruction must be a string or None.")
281
-
282
- margins: list[float] = []
283
- for start in range(0, len(validated_pairs), self.batch_size):
284
- batch = validated_pairs[start : start + self.batch_size]
285
- prompts = [
286
- self._prompt(query, document, effective_instruction)
287
- for query, document in batch
288
- ]
289
- if self.llm is None:
290
- raise RuntimeError("The reranker has been closed.")
291
- outputs = self.llm.classify(
292
- prompts,
293
- pooling_params=self.pooling_params,
294
- use_tqdm=False,
295
- )
296
- margins.extend(self._margins_from_outputs(outputs))
297
-
298
- scores = [_sigmoid(margin) for margin in margins]
299
- if return_margin:
300
- return [
301
- {"score": score, "margin": margin}
302
- for score, margin in zip(scores, margins)
303
- ]
304
- return scores
305
-
306
- def rank(
307
- self,
308
- query: str,
309
- documents: Sequence[str],
310
- *,
311
- instruction: Optional[str] = None,
312
- top_k: Optional[int] = None,
313
- return_margin: bool = False,
314
- ) -> list[dict[str, float | int]]:
315
- if not isinstance(query, str):
316
- raise TypeError("query must be a string.")
317
- if isinstance(documents, (str, bytes)) or not isinstance(documents, Sequence):
318
- raise TypeError("documents must be a sequence of strings.")
319
- if any(not isinstance(document, str) for document in documents):
320
- raise TypeError("every document must be a string.")
321
- if top_k is not None:
322
- top_k = int(top_k)
323
- if top_k < 0:
324
- raise ValueError("top_k must be non-negative or None.")
325
-
326
- predictions = self.predict(
327
- [(query, document) for document in documents],
328
- instruction=instruction,
329
- return_margin=return_margin,
330
- )
331
- rankings: list[dict[str, float | int]] = []
332
- for corpus_id, prediction in enumerate(predictions):
333
- if return_margin:
334
- assert isinstance(prediction, dict)
335
- item: dict[str, float | int] = {
336
- "corpus_id": corpus_id,
337
- "score": prediction["score"],
338
- "margin": prediction["margin"],
339
- }
340
- else:
341
- assert isinstance(prediction, float)
342
- item = {"corpus_id": corpus_id, "score": prediction}
343
- rankings.append(item)
344
- rankings.sort(key=lambda item: float(item["score"]), reverse=True)
345
- return rankings if top_k is None else rankings[:top_k]
346
-
347
-
348
- KaLMVLLMOfflineReranker = KaLMVLLMReranker
349
-
350
- __all__ = [
351
- "KaLMVLLMOfflineReranker",
352
- "KaLMVLLMReranker",
353
- "SUPPORTED_ENCODER_CHUNK_SIZES",
354
- "build_hf_overrides",
355
- "check_runtime",
356
- "parse_encoder_chunk_size",
357
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
vllm_support/build/lib/kalm_t5gemma2_vllm_plugin/server.py DELETED
@@ -1,215 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
- import threading
5
- import time
6
- from pathlib import Path
7
- from typing import Any, Optional
8
-
9
- from fastapi import FastAPI, HTTPException
10
- from pydantic import BaseModel, Field
11
-
12
- from .constants import (
13
- DEFAULT_DOCUMENT_MAX_LENGTH,
14
- DEFAULT_ENCODER_CHUNK_SIZE,
15
- DEFAULT_MAX_MODEL_LEN,
16
- DEFAULT_QUERY_MAX_LENGTH,
17
- MODEL_ID,
18
- SUPPORTED_ENCODER_CHUNK_SIZES,
19
- parse_encoder_chunk_size,
20
- )
21
- from .reranker import KaLMVLLMReranker
22
-
23
-
24
- class RerankRequest(BaseModel):
25
- query: str
26
- documents: list[str] = Field(min_length=1)
27
- instruction: Optional[str] = None
28
- top_k: Optional[int] = None
29
- return_margin: bool = False
30
-
31
-
32
- class ScorePair(BaseModel):
33
- query: str
34
- document: str
35
- id: Optional[Any] = None
36
-
37
-
38
- class ScoreRequest(BaseModel):
39
- pairs: list[ScorePair] = Field(min_length=1)
40
- instruction: Optional[str] = None
41
- return_margin: bool = False
42
-
43
-
44
- class OnlineRerankerService:
45
- def __init__(self, args: argparse.Namespace) -> None:
46
- self.model = str(args.model)
47
- self.query_max_length = int(args.query_max_length)
48
- self.document_max_length = int(args.document_max_length)
49
- self.encoder_chunk_size = parse_encoder_chunk_size(args.encoder_chunk_size)
50
- self.max_model_len = int(args.max_model_len)
51
- self.batch_size = int(args.batch_size)
52
- self.dtype = str(args.dtype)
53
- self.gpu_memory_utilization = float(args.gpu_memory_utilization)
54
- self.started_at = time.time()
55
- self.lock = threading.Lock()
56
- self.reranker = KaLMVLLMReranker(
57
- self.model,
58
- query_max_length=self.query_max_length,
59
- document_max_length=self.document_max_length,
60
- encoder_chunk_size=self.encoder_chunk_size,
61
- max_model_len=self.max_model_len,
62
- batch_size=self.batch_size,
63
- dtype=self.dtype,
64
- gpu_memory_utilization=self.gpu_memory_utilization,
65
- tensor_parallel_size=args.tensor_parallel_size,
66
- )
67
-
68
- def config(self) -> dict[str, Any]:
69
- return {
70
- "model": self.model,
71
- "query_max_length": self.query_max_length,
72
- "document_max_length": self.document_max_length,
73
- "encoder_chunk_size": self.encoder_chunk_size,
74
- "max_model_len": self.max_model_len,
75
- "batch_size": self.batch_size,
76
- "dtype": self.dtype,
77
- "gpu_memory_utilization": self.gpu_memory_utilization,
78
- "tensor_parallel_size": 1,
79
- "supported_encoder_chunk_sizes": sorted(
80
- SUPPORTED_ENCODER_CHUNK_SIZES
81
- ),
82
- }
83
-
84
- def health(self) -> dict[str, Any]:
85
- return {
86
- "status": "ok",
87
- "uptime_seconds": round(time.time() - self.started_at, 3),
88
- **self.config(),
89
- }
90
-
91
- def close(self) -> None:
92
- self.reranker.close()
93
-
94
- def rerank(self, request: RerankRequest) -> dict[str, Any]:
95
- if request.top_k is not None and request.top_k < 0:
96
- raise ValueError("top_k must be non-negative or null.")
97
- with self.lock:
98
- rankings = self.reranker.rank(
99
- request.query,
100
- request.documents,
101
- instruction=request.instruction,
102
- top_k=request.top_k,
103
- return_margin=request.return_margin,
104
- )
105
- results: list[dict[str, Any]] = []
106
- for item in rankings:
107
- result = {
108
- "index": int(item["corpus_id"]),
109
- "score": float(item["score"]),
110
- }
111
- if request.return_margin:
112
- result["margin"] = float(item["margin"])
113
- results.append(result)
114
- return {"object": "rerank", "results": results}
115
-
116
- def score(self, request: ScoreRequest) -> dict[str, Any]:
117
- pairs = [(item.query, item.document) for item in request.pairs]
118
- with self.lock:
119
- predictions = self.reranker.predict(
120
- pairs,
121
- instruction=request.instruction,
122
- return_margin=True,
123
- )
124
- results: list[dict[str, Any]] = []
125
- for index, (pair, prediction) in enumerate(zip(request.pairs, predictions)):
126
- assert isinstance(prediction, dict)
127
- result = {
128
- "index": index,
129
- "score": float(prediction["score"]),
130
- }
131
- if pair.id is not None:
132
- result["id"] = pair.id
133
- if request.return_margin:
134
- result["margin"] = float(prediction["margin"])
135
- results.append(result)
136
- return {"object": "score", "results": results}
137
-
138
-
139
- def build_parser() -> argparse.ArgumentParser:
140
- parser = argparse.ArgumentParser(
141
- description="FastAPI server for the KaLM vLLM adapter.",
142
- formatter_class=argparse.ArgumentDefaultsHelpFormatter,
143
- )
144
- parser.add_argument("--host", default="0.0.0.0")
145
- parser.add_argument("--port", type=int, default=8000)
146
- parser.add_argument("--model", default=MODEL_ID)
147
- parser.add_argument("--query-max-length", type=int, default=DEFAULT_QUERY_MAX_LENGTH)
148
- parser.add_argument(
149
- "--document-max-length", type=int, default=DEFAULT_DOCUMENT_MAX_LENGTH
150
- )
151
- parser.add_argument(
152
- "--encoder-chunk-size", default=str(DEFAULT_ENCODER_CHUNK_SIZE)
153
- )
154
- parser.add_argument("--max-model-len", type=int, default=DEFAULT_MAX_MODEL_LEN)
155
- parser.add_argument("--batch-size", type=int, default=32)
156
- parser.add_argument("--dtype", default="bfloat16")
157
- parser.add_argument("--gpu-memory-utilization", type=float, default=0.85)
158
- parser.add_argument("--tensor-parallel-size", type=int, default=1)
159
- parser.add_argument("--log-level", default="info")
160
- return parser
161
-
162
-
163
- def create_app(service: OnlineRerankerService) -> FastAPI:
164
- app = FastAPI(title="KaLM vLLM Online Reranker", version="0.1.0")
165
- app.state.service = service
166
- app.add_event_handler("shutdown", service.close)
167
-
168
- @app.get("/health")
169
- def health():
170
- return app.state.service.health()
171
-
172
- @app.post("/rerank")
173
- def rerank(request: RerankRequest):
174
- try:
175
- return app.state.service.rerank(request)
176
- except (TypeError, ValueError) as error:
177
- raise HTTPException(status_code=400, detail=str(error)) from error
178
-
179
- @app.post("/score")
180
- def score(request: ScoreRequest):
181
- try:
182
- return app.state.service.score(request)
183
- except (TypeError, ValueError) as error:
184
- raise HTTPException(status_code=400, detail=str(error)) from error
185
-
186
- return app
187
-
188
-
189
- def main() -> int:
190
- args = build_parser().parse_args()
191
- args.encoder_chunk_size = parse_encoder_chunk_size(args.encoder_chunk_size)
192
- print("=== KaLM vLLM Online Reranker ===", flush=True)
193
- print(f"model: {args.model}", flush=True)
194
- print(f"query_max_length: {args.query_max_length}", flush=True)
195
- print(f"document_max_length: {args.document_max_length}", flush=True)
196
- print(f"encoder_chunk_size: {args.encoder_chunk_size}", flush=True)
197
- print(f"max_model_len: {args.max_model_len}", flush=True)
198
- print(f"listen: http://{args.host}:{args.port}", flush=True)
199
-
200
- service = OnlineRerankerService(args)
201
- app = create_app(service)
202
- import uvicorn
203
-
204
- uvicorn.run(
205
- app,
206
- host=args.host,
207
- port=args.port,
208
- log_level=args.log_level,
209
- workers=1,
210
- )
211
- return 0
212
-
213
-
214
- if __name__ == "__main__":
215
- raise SystemExit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
vllm_support/src/kalm_t5gemma2_vllm_plugin.egg-info/PKG-INFO DELETED
@@ -1,292 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: kalm-t5gemma2-vllm-plugin
3
- Version: 0.1.0
4
- Summary: vLLM 0.19.1 adapter for KaLM-Reranker-V1-Nano
5
- License: Apache-2.0
6
- Requires-Python: <3.13,>=3.12
7
- Description-Content-Type: text/markdown
8
- Requires-Dist: transformers==5.6.2
9
- Requires-Dist: vllm==0.19.1
10
- Provides-Extra: online
11
- Requires-Dist: fastapi<0.137,>=0.136; extra == "online"
12
- Requires-Dist: uvicorn<0.47,>=0.46; extra == "online"
13
-
14
- # KaLM-Reranker-V1-Nano vLLM Support
15
-
16
- This directory contains the experimental vLLM 0.19.1 adapter for
17
- `KaLM-Embedding/KaLM-Reranker-V1-Nano`. It supports offline Python and CLI
18
- reranking plus an optional FastAPI service.
19
-
20
- The adapter does not modify or retrain the checkpoint. It reads the original
21
- decoder logits for the single-token answers `yes` and `no` and returns:
22
-
23
- ```text
24
- margin = yes_logit - no_logit
25
- score = sigmoid(margin) = P(yes)
26
- ```
27
-
28
- ## Tested environment
29
-
30
- - Linux and NVIDIA CUDA
31
- - Python 3.12
32
- - vLLM 0.19.1
33
- - Transformers 5.6.2
34
- - PyTorch 2.10.0
35
- - BF16, one GPU
36
-
37
- The package intentionally rejects other vLLM versions and
38
- `tensor_parallel_size != 1`. These combinations have not been validated.
39
-
40
- ## Installation
41
-
42
- Create an environment and download the model repository:
43
-
44
- ```bash
45
- conda create -n kalm-vllm python=3.12 -y
46
- conda activate kalm-vllm
47
-
48
- pip install "vllm==0.19.1" "transformers==5.6.2"
49
- pip install "fastapi>=0.136,<0.137" "uvicorn>=0.46,<0.47"
50
-
51
- hf download KaLM-Embedding/KaLM-Reranker-V1-Nano \
52
- --local-dir ./KaLM-Reranker-V1-Nano
53
- pip install ./KaLM-Reranker-V1-Nano/vllm_support --no-deps
54
- export VLLM_PLUGINS=kalm_t5gemma2
55
- ```
56
-
57
- The model can also be loaded directly by its Hugging Face ID. In that case,
58
- only download the `vllm_support` directory before installing the plugin:
59
-
60
- ```bash
61
- hf download KaLM-Embedding/KaLM-Reranker-V1-Nano \
62
- --include "vllm_support/**" \
63
- --local-dir ./KaLM-Reranker-V1-Nano
64
- pip install ./KaLM-Reranker-V1-Nano/vllm_support --no-deps
65
- ```
66
-
67
- ## Offline Python API
68
-
69
- ```python
70
- from kalm_t5gemma2_vllm_plugin import KaLMVLLMReranker
71
-
72
- query = "What is the capital of China?"
73
- documents = [
74
- "The capital of China is Beijing.",
75
- "Gravity attracts bodies toward one another.",
76
- ]
77
-
78
- pairs = [(query, document) for document in documents]
79
- with KaLMVLLMReranker(
80
- "KaLM-Embedding/KaLM-Reranker-V1-Nano",
81
- query_max_length=512,
82
- document_max_length=1024,
83
- encoder_chunk_size=4,
84
- max_model_len=2048,
85
- batch_size=32,
86
- ) as reranker:
87
- print(reranker.predict(pairs))
88
- print(reranker.predict(pairs, return_margin=True))
89
- print(reranker.rank(query, documents))
90
- ```
91
-
92
- Expected BF16 scores are approximately:
93
-
94
- ```text
95
- [0.99980897, 0.00000493699]
96
- ```
97
-
98
- `predict()` preserves input order. `rank()` returns score-descending results
99
- with the original document index in `corpus_id`.
100
-
101
- ## Offline CLI
102
-
103
- Run the built-in example:
104
-
105
- ```bash
106
- kalm-vllm-rerank --return-margin
107
- ```
108
-
109
- Score JSONL input:
110
-
111
- ```bash
112
- kalm-vllm-rerank \
113
- --input-jsonl ./KaLM-Reranker-V1-Nano/vllm_support/examples/sample_pairs.jsonl \
114
- --output-jsonl ./scores.jsonl \
115
- --return-margin
116
- ```
117
-
118
- Each input line must contain `query` and `document`. Optional fields are `id`
119
- and `instruction`. `--top-k N` groups rows by exact query text, sorts each
120
- group by score, and keeps its first `N` documents.
121
-
122
- ## Online service
123
-
124
- Start one model instance:
125
-
126
- ```bash
127
- kalm-vllm-serve \
128
- --host 0.0.0.0 \
129
- --port 8000 \
130
- --model KaLM-Embedding/KaLM-Reranker-V1-Nano \
131
- --encoder-chunk-size 4
132
- ```
133
-
134
- The portable startup script exposes the same settings through environment
135
- variables:
136
-
137
- ```bash
138
- CUDA_VISIBLE_DEVICES=0 PORT=8000 \
139
- ./KaLM-Reranker-V1-Nano/vllm_support/examples/start_online_server.sh
140
- ```
141
-
142
- In a second terminal, check health and send built-in demo requests:
143
-
144
- ```bash
145
- kalm-vllm-client --health
146
- kalm-vllm-client --endpoint rerank --return-margin
147
- kalm-vllm-client --endpoint score --return-margin
148
- ```
149
-
150
- For custom input, pass one JSON object with `--json-file`. Use `/rerank` for
151
- one query against multiple documents:
152
-
153
- ```bash
154
- kalm-vllm-client \
155
- --endpoint rerank \
156
- --json-file ./KaLM-Reranker-V1-Nano/vllm_support/examples/rerank_request.json \
157
- --return-margin \
158
- --top-k 10
159
- ```
160
-
161
- Use `/score` for a batch of independent query-document pairs:
162
-
163
- ```bash
164
- kalm-vllm-client \
165
- --endpoint score \
166
- --json-file ./KaLM-Reranker-V1-Nano/vllm_support/examples/score_request.json \
167
- --return-margin
168
- ```
169
-
170
- When `--json-file` is used, `--return-margin` sets
171
- `"return_margin": true` in the outgoing request, and `--top-k` overrides the
172
- JSON value for `/rerank`.
173
-
174
- ### `POST /rerank`
175
-
176
- ```json
177
- {
178
- "query": "What is the capital of China?",
179
- "documents": [
180
- "The capital of China is Beijing.",
181
- "Gravity attracts bodies toward one another."
182
- ],
183
- "instruction": "Given a query, retrieve documents that answer the query.",
184
- "top_k": null,
185
- "return_margin": true
186
- }
187
- ```
188
-
189
- Results are returned in descending score order:
190
-
191
- ```json
192
- {
193
- "object": "rerank",
194
- "results": [
195
- {"index": 0, "score": 0.9998089, "margin": 8.5625},
196
- {"index": 1, "score": 0.00000493699, "margin": -12.21875}
197
- ]
198
- }
199
- ```
200
-
201
- ### `POST /score`
202
-
203
- ```json
204
- {
205
- "pairs": [
206
- {
207
- "id": "doc-1",
208
- "query": "What is the capital of China?",
209
- "document": "The capital of China is Beijing."
210
- }
211
- ],
212
- "instruction": null,
213
- "return_margin": false
214
- }
215
- ```
216
-
217
- `/score` accepts multiple entries in `pairs`, preserves their input order and
218
- includes an input `id` when provided.
219
-
220
- ### `GET /health`
221
-
222
- Returns service status and the effective model, length, chunking, dtype and
223
- memory settings.
224
-
225
- ## Configuration
226
-
227
- | Setting | Default | Meaning |
228
- | --- | ---: | --- |
229
- | `query_max_length` | `512` | Maximum raw query tokens before prompt insertion |
230
- | `document_max_length` | `1024` | Maximum encoder tokens for `<Document>: ...` |
231
- | `encoder_chunk_size` | `4` | Mean-pooling factor; one of `1,2,4,8,16,32` |
232
- | `max_model_len` | `2048` | vLLM engine context budget |
233
- | `batch_size` | `32` | Pairs passed to each `LLM.classify()` call |
234
- | `dtype` | `bfloat16` | Model compute dtype |
235
- | `gpu_memory_utilization` | `0.85` | vLLM GPU memory fraction |
236
- | `tensor_parallel_size` | `1` | Only supported value in this release |
237
-
238
- The query and document limits belong to separate decoder and encoder streams;
239
- they are not a combined cross-encoder token limit. Larger values are
240
- configurable but have not been validated up to the model card's full 128K
241
- limit.
242
-
243
- ## Limitations
244
-
245
- - This is a custom `LLM.classify()` plugin, not vLLM's native HTTP `/score`
246
- implementation.
247
- - The shim uses vLLM scheduling and pooling interfaces but executes the
248
- T5Gemma2 semantic forward through Transformers. It is not a complete
249
- vLLM-native kernel implementation and should not be used to claim native
250
- vLLM throughput.
251
- - Online serving is a single-process FastAPI wrapper around one model instance.
252
- - `encoder_chunk_size=None`, `null`, or an empty string falls back to `4`; it
253
- does not disable pooling in this release.
254
-
255
- ## Troubleshooting
256
-
257
- **The plugin is not discovered**
258
-
259
- Reinstall the package and ensure the environment variable includes its entry
260
- point name:
261
-
262
- ```bash
263
- pip install ./KaLM-Reranker-V1-Nano/vllm_support --no-deps --force-reinstall
264
- export VLLM_PLUGINS=kalm_t5gemma2
265
- ```
266
-
267
- **The adapter reports an unsupported vLLM version**
268
-
269
- Install exactly `vllm==0.19.1`. Internal model and processor APIs are version
270
- sensitive.
271
-
272
- **The tokenizer check fails**
273
-
274
- Confirm that the tokenizer belongs to this Nano checkpoint. The adapter
275
- requires `yes -> 4443` and `no -> 1904`.
276
-
277
- **CUDA runs out of memory**
278
-
279
- Reduce `batch_size`, `document_max_length`, `query_max_length`,
280
- `max_model_len`, or `gpu_memory_utilization`.
281
-
282
- **CUDA initialization fails with error 803**
283
-
284
- The process may be resolving a CUDA compatibility library before the host
285
- driver library. On common Debian/Ubuntu layouts, retry with:
286
-
287
- ```bash
288
- export LD_LIBRARY_PATH="/lib/x86_64-linux-gnu:/usr/lib/x86_64-linux-gnu${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
289
- ```
290
-
291
- The provided `start_online_server.sh` applies this adjustment automatically
292
- when both directories exist.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
vllm_support/src/kalm_t5gemma2_vllm_plugin.egg-info/SOURCES.txt DELETED
@@ -1,18 +0,0 @@
1
- README.md
2
- pyproject.toml
3
- src/kalm_t5gemma2_vllm_plugin/__init__.py
4
- src/kalm_t5gemma2_vllm_plugin/cli.py
5
- src/kalm_t5gemma2_vllm_plugin/client.py
6
- src/kalm_t5gemma2_vllm_plugin/constants.py
7
- src/kalm_t5gemma2_vllm_plugin/modeling.py
8
- src/kalm_t5gemma2_vllm_plugin/modeling_score.py
9
- src/kalm_t5gemma2_vllm_plugin/processing.py
10
- src/kalm_t5gemma2_vllm_plugin/register.py
11
- src/kalm_t5gemma2_vllm_plugin/reranker.py
12
- src/kalm_t5gemma2_vllm_plugin/server.py
13
- src/kalm_t5gemma2_vllm_plugin.egg-info/PKG-INFO
14
- src/kalm_t5gemma2_vllm_plugin.egg-info/SOURCES.txt
15
- src/kalm_t5gemma2_vllm_plugin.egg-info/dependency_links.txt
16
- src/kalm_t5gemma2_vllm_plugin.egg-info/entry_points.txt
17
- src/kalm_t5gemma2_vllm_plugin.egg-info/requires.txt
18
- src/kalm_t5gemma2_vllm_plugin.egg-info/top_level.txt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
vllm_support/src/kalm_t5gemma2_vllm_plugin.egg-info/dependency_links.txt DELETED
@@ -1 +0,0 @@
1
-
 
 
vllm_support/src/kalm_t5gemma2_vllm_plugin.egg-info/entry_points.txt DELETED
@@ -1,7 +0,0 @@
1
- [console_scripts]
2
- kalm-vllm-client = kalm_t5gemma2_vllm_plugin.client:main
3
- kalm-vllm-rerank = kalm_t5gemma2_vllm_plugin.cli:main
4
- kalm-vllm-serve = kalm_t5gemma2_vllm_plugin.server:main
5
-
6
- [vllm.general_plugins]
7
- kalm_t5gemma2 = kalm_t5gemma2_vllm_plugin.register:register
 
 
 
 
 
 
 
 
vllm_support/src/kalm_t5gemma2_vllm_plugin.egg-info/requires.txt DELETED
@@ -1,6 +0,0 @@
1
- transformers==5.6.2
2
- vllm==0.19.1
3
-
4
- [online]
5
- fastapi<0.137,>=0.136
6
- uvicorn<0.47,>=0.46
 
 
 
 
 
 
 
vllm_support/src/kalm_t5gemma2_vllm_plugin.egg-info/top_level.txt DELETED
@@ -1 +0,0 @@
1
- kalm_t5gemma2_vllm_plugin
 
 
vllm_support/src/kalm_t5gemma2_vllm_plugin/client.py CHANGED
@@ -102,7 +102,7 @@ def _request_payload(args: argparse.Namespace) -> dict[str, Any]:
102
  payload["top_k"] = args.top_k
103
  return payload
104
 
105
-
106
  def main() -> int:
107
  args = build_parser().parse_args()
108
  base_url = args.base_url.rstrip("/")
 
102
  payload["top_k"] = args.top_k
103
  return payload
104
 
105
+
106
  def main() -> int:
107
  args = build_parser().parse_args()
108
  base_url = args.base_url.rstrip("/")
vllm_support/src/kalm_t5gemma2_vllm_plugin/server.py CHANGED
@@ -163,7 +163,7 @@ def build_parser() -> argparse.ArgumentParser:
163
  def create_app(service: OnlineRerankerService) -> FastAPI:
164
  app = FastAPI(title="KaLM vLLM Online Reranker", version="0.1.0")
165
  app.state.service = service
166
- app.add_event_handler("shutdown", service.close)
167
 
168
  @app.get("/health")
169
  def health():
 
163
  def create_app(service: OnlineRerankerService) -> FastAPI:
164
  app = FastAPI(title="KaLM vLLM Online Reranker", version="0.1.0")
165
  app.state.service = service
166
+ app.router.add_event_handler("shutdown", service.close)
167
 
168
  @app.get("/health")
169
  def health():