Text Ranking
Transformers
Safetensors
multilingual
t5gemma2
text2text-generation
reranker
encoder-decoder
FBNL
Retrieval
RAG
Instructions to use KaLM-Embedding/KaLM-Reranker-V1-Large with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use KaLM-Embedding/KaLM-Reranker-V1-Large with Transformers:
# Load model directly from transformers import AutoProcessor, AutoModelForMultimodalLM processor = AutoProcessor.from_pretrained("KaLM-Embedding/KaLM-Reranker-V1-Large") model = AutoModelForMultimodalLM.from_pretrained("KaLM-Embedding/KaLM-Reranker-V1-Large", device_map="auto") - Notebooks
- Google Colab
- Kaggle
Upload 34 files
Browse files- vllm_support/README.md +279 -0
- vllm_support/build/lib/kalm_t5gemma2_vllm_plugin/__init__.py +23 -0
- vllm_support/build/lib/kalm_t5gemma2_vllm_plugin/cli.py +172 -0
- vllm_support/build/lib/kalm_t5gemma2_vllm_plugin/client.py +125 -0
- vllm_support/build/lib/kalm_t5gemma2_vllm_plugin/constants.py +105 -0
- vllm_support/build/lib/kalm_t5gemma2_vllm_plugin/modeling.py +265 -0
- vllm_support/build/lib/kalm_t5gemma2_vllm_plugin/modeling_score.py +189 -0
- vllm_support/build/lib/kalm_t5gemma2_vllm_plugin/processing.py +173 -0
- vllm_support/build/lib/kalm_t5gemma2_vllm_plugin/register.py +15 -0
- vllm_support/build/lib/kalm_t5gemma2_vllm_plugin/reranker.py +357 -0
- vllm_support/build/lib/kalm_t5gemma2_vllm_plugin/server.py +215 -0
- vllm_support/examples/call_online_api.py +6 -0
- vllm_support/examples/offline_usage.py +17 -0
- vllm_support/examples/rerank_request.json +10 -0
- vllm_support/examples/sample_pairs.jsonl +2 -0
- vllm_support/examples/score_request.json +16 -0
- vllm_support/examples/start_online_server.sh +20 -0
- vllm_support/pyproject.toml +33 -0
- vllm_support/src/kalm_t5gemma2_vllm_plugin.egg-info/PKG-INFO +292 -0
- vllm_support/src/kalm_t5gemma2_vllm_plugin.egg-info/SOURCES.txt +18 -0
- vllm_support/src/kalm_t5gemma2_vllm_plugin.egg-info/dependency_links.txt +1 -0
- vllm_support/src/kalm_t5gemma2_vllm_plugin.egg-info/entry_points.txt +7 -0
- vllm_support/src/kalm_t5gemma2_vllm_plugin.egg-info/requires.txt +6 -0
- vllm_support/src/kalm_t5gemma2_vllm_plugin.egg-info/top_level.txt +1 -0
- vllm_support/src/kalm_t5gemma2_vllm_plugin/__init__.py +23 -0
- vllm_support/src/kalm_t5gemma2_vllm_plugin/cli.py +172 -0
- vllm_support/src/kalm_t5gemma2_vllm_plugin/client.py +125 -0
- vllm_support/src/kalm_t5gemma2_vllm_plugin/constants.py +105 -0
- vllm_support/src/kalm_t5gemma2_vllm_plugin/modeling.py +265 -0
- vllm_support/src/kalm_t5gemma2_vllm_plugin/modeling_score.py +189 -0
- vllm_support/src/kalm_t5gemma2_vllm_plugin/processing.py +173 -0
- vllm_support/src/kalm_t5gemma2_vllm_plugin/register.py +15 -0
- vllm_support/src/kalm_t5gemma2_vllm_plugin/reranker.py +357 -0
- vllm_support/src/kalm_t5gemma2_vllm_plugin/server.py +215 -0
vllm_support/README.md
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 8 |
+
decoder logits for the single-token answers `yes` and `no` and returns:
|
| 9 |
+
|
| 10 |
+
```text
|
| 11 |
+
margin = yes_logit - no_logit
|
| 12 |
+
score = sigmoid(margin) = P(yes)
|
| 13 |
+
```
|
| 14 |
+
|
| 15 |
+
## Tested environment
|
| 16 |
+
|
| 17 |
+
- Linux and NVIDIA CUDA
|
| 18 |
+
- Python 3.12
|
| 19 |
+
- vLLM 0.19.1
|
| 20 |
+
- Transformers 5.6.2
|
| 21 |
+
- PyTorch 2.10.0
|
| 22 |
+
- BF16, one GPU
|
| 23 |
+
|
| 24 |
+
The package intentionally rejects other vLLM versions and
|
| 25 |
+
`tensor_parallel_size != 1`. These combinations have not been validated.
|
| 26 |
+
|
| 27 |
+
## Installation
|
| 28 |
+
|
| 29 |
+
Create an environment and download the model repository:
|
| 30 |
+
|
| 31 |
+
```bash
|
| 32 |
+
conda create -n kalm-vllm python=3.12 -y
|
| 33 |
+
conda activate kalm-vllm
|
| 34 |
+
|
| 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 |
+
|
| 44 |
+
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
|
| 55 |
+
|
| 56 |
+
```python
|
| 57 |
+
from kalm_t5gemma2_vllm_plugin import KaLMVLLMReranker
|
| 58 |
+
|
| 59 |
+
query = "What is the capital of China?"
|
| 60 |
+
documents = [
|
| 61 |
+
"The capital of China is Beijing.",
|
| 62 |
+
"Gravity attracts bodies toward one another.",
|
| 63 |
+
]
|
| 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,
|
| 71 |
+
max_model_len=2048,
|
| 72 |
+
batch_size=32,
|
| 73 |
+
) as reranker:
|
| 74 |
+
print(reranker.predict(pairs))
|
| 75 |
+
print(reranker.predict(pairs, return_margin=True))
|
| 76 |
+
print(reranker.rank(query, documents))
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
Expected BF16 scores are approximately:
|
| 80 |
+
|
| 81 |
+
```text
|
| 82 |
+
[0.99980897, 0.00000493699]
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
`predict()` preserves input order. `rank()` returns score-descending results
|
| 86 |
+
with the original document index in `corpus_id`.
|
| 87 |
+
|
| 88 |
+
## Offline CLI
|
| 89 |
+
|
| 90 |
+
Run the built-in example:
|
| 91 |
+
|
| 92 |
+
```bash
|
| 93 |
+
kalm-vllm-rerank --return-margin
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
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 |
+
```
|
| 104 |
+
|
| 105 |
+
Each input line must contain `query` and `document`. Optional fields are `id`
|
| 106 |
+
and `instruction`. `--top-k N` groups rows by exact query text, sorts each
|
| 107 |
+
group by score, and keeps its first `N` documents.
|
| 108 |
+
|
| 109 |
+
## Online service
|
| 110 |
+
|
| 111 |
+
Start one model instance:
|
| 112 |
+
|
| 113 |
+
```bash
|
| 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 |
+
|
| 121 |
+
The portable startup script exposes the same settings through environment
|
| 122 |
+
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:
|
| 130 |
+
|
| 131 |
+
```bash
|
| 132 |
+
kalm-vllm-client --health
|
| 133 |
+
kalm-vllm-client --endpoint rerank --return-margin
|
| 134 |
+
kalm-vllm-client --endpoint score --return-margin
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
For custom input, pass one JSON object with `--json-file`. Use `/rerank` for
|
| 138 |
+
one query against multiple documents:
|
| 139 |
+
|
| 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 |
+
```
|
| 147 |
+
|
| 148 |
+
Use `/score` for a batch of independent query-document pairs:
|
| 149 |
+
|
| 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 |
+
|
| 157 |
+
When `--json-file` is used, `--return-margin` sets
|
| 158 |
+
`"return_margin": true` in the outgoing request, and `--top-k` overrides the
|
| 159 |
+
JSON value for `/rerank`.
|
| 160 |
+
|
| 161 |
+
### `POST /rerank`
|
| 162 |
+
|
| 163 |
+
```json
|
| 164 |
+
{
|
| 165 |
+
"query": "What is the capital of China?",
|
| 166 |
+
"documents": [
|
| 167 |
+
"The capital of China is Beijing.",
|
| 168 |
+
"Gravity attracts bodies toward one another."
|
| 169 |
+
],
|
| 170 |
+
"instruction": "Given a query, retrieve documents that answer the query.",
|
| 171 |
+
"top_k": null,
|
| 172 |
+
"return_margin": true
|
| 173 |
+
}
|
| 174 |
+
```
|
| 175 |
+
|
| 176 |
+
Results are returned in descending score order:
|
| 177 |
+
|
| 178 |
+
```json
|
| 179 |
+
{
|
| 180 |
+
"object": "rerank",
|
| 181 |
+
"results": [
|
| 182 |
+
{"index": 0, "score": 0.9998089, "margin": 8.5625},
|
| 183 |
+
{"index": 1, "score": 0.00000493699, "margin": -12.21875}
|
| 184 |
+
]
|
| 185 |
+
}
|
| 186 |
+
```
|
| 187 |
+
|
| 188 |
+
### `POST /score`
|
| 189 |
+
|
| 190 |
+
```json
|
| 191 |
+
{
|
| 192 |
+
"pairs": [
|
| 193 |
+
{
|
| 194 |
+
"id": "doc-1",
|
| 195 |
+
"query": "What is the capital of China?",
|
| 196 |
+
"document": "The capital of China is Beijing."
|
| 197 |
+
}
|
| 198 |
+
],
|
| 199 |
+
"instruction": null,
|
| 200 |
+
"return_margin": false
|
| 201 |
+
}
|
| 202 |
+
```
|
| 203 |
+
|
| 204 |
+
`/score` accepts multiple entries in `pairs`, preserves their input order and
|
| 205 |
+
includes an input `id` when provided.
|
| 206 |
+
|
| 207 |
+
### `GET /health`
|
| 208 |
+
|
| 209 |
+
Returns service status and the effective model, length, chunking, dtype and
|
| 210 |
+
memory settings.
|
| 211 |
+
|
| 212 |
+
## Configuration
|
| 213 |
+
|
| 214 |
+
| Setting | Default | Meaning |
|
| 215 |
+
| --- | ---: | --- |
|
| 216 |
+
| `query_max_length` | `512` | Maximum raw query tokens before prompt insertion |
|
| 217 |
+
| `document_max_length` | `1024` | Maximum encoder tokens for `<Document>: ...` |
|
| 218 |
+
| `encoder_chunk_size` | `4` | Mean-pooling factor; one of `1,2,4,8,16,32` |
|
| 219 |
+
| `max_model_len` | `2048` | vLLM engine context budget |
|
| 220 |
+
| `batch_size` | `32` | Pairs passed to each `LLM.classify()` call |
|
| 221 |
+
| `dtype` | `bfloat16` | Model compute dtype |
|
| 222 |
+
| `gpu_memory_utilization` | `0.85` | vLLM GPU memory fraction |
|
| 223 |
+
| `tensor_parallel_size` | `1` | Only supported value in this release |
|
| 224 |
+
|
| 225 |
+
The query and document limits belong to separate decoder and encoder streams;
|
| 226 |
+
they are not a combined cross-encoder token limit. Larger values are
|
| 227 |
+
configurable but have not been validated up to the model card's full 128K
|
| 228 |
+
limit.
|
| 229 |
+
|
| 230 |
+
## Limitations
|
| 231 |
+
|
| 232 |
+
- This is a custom `LLM.classify()` plugin, not vLLM's native HTTP `/score`
|
| 233 |
+
implementation.
|
| 234 |
+
- The shim uses vLLM scheduling and pooling interfaces but executes the
|
| 235 |
+
T5Gemma2 semantic forward through Transformers. It is not a complete
|
| 236 |
+
vLLM-native kernel implementation and should not be used to claim native
|
| 237 |
+
vLLM throughput.
|
| 238 |
+
- Online serving is a single-process FastAPI wrapper around one model instance.
|
| 239 |
+
- `encoder_chunk_size=None`, `null`, or an empty string falls back to `4`; it
|
| 240 |
+
does not disable pooling in this release.
|
| 241 |
+
|
| 242 |
+
## Troubleshooting
|
| 243 |
+
|
| 244 |
+
**The plugin is not discovered**
|
| 245 |
+
|
| 246 |
+
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 |
+
|
| 254 |
+
**The adapter reports an unsupported vLLM version**
|
| 255 |
+
|
| 256 |
+
Install exactly `vllm==0.19.1`. Internal model and processor APIs are version
|
| 257 |
+
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**
|
| 265 |
+
|
| 266 |
+
Reduce `batch_size`, `document_max_length`, `query_max_length`,
|
| 267 |
+
`max_model_len`, or `gpu_memory_utilization`.
|
| 268 |
+
|
| 269 |
+
**CUDA initialization fails with error 803**
|
| 270 |
+
|
| 271 |
+
The process may be resolving a CUDA compatibility library before the host
|
| 272 |
+
driver library. On common Debian/Ubuntu layouts, retry with:
|
| 273 |
+
|
| 274 |
+
```bash
|
| 275 |
+
export LD_LIBRARY_PATH="/lib/x86_64-linux-gnu:/usr/lib/x86_64-linux-gnu${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
| 276 |
+
```
|
| 277 |
+
|
| 278 |
+
The provided `start_online_server.sh` applies this adjustment automatically
|
| 279 |
+
when both directories exist.
|
vllm_support/build/lib/kalm_t5gemma2_vllm_plugin/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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/examples/call_online_api.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from kalm_t5gemma2_vllm_plugin.client import main
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
if __name__ == "__main__":
|
| 5 |
+
raise SystemExit(main())
|
| 6 |
+
|
vllm_support/examples/offline_usage.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from kalm_t5gemma2_vllm_plugin import KaLMVLLMReranker
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
query = "What is the capital of China?"
|
| 5 |
+
documents = [
|
| 6 |
+
"The capital of China is Beijing.",
|
| 7 |
+
"Gravity attracts bodies toward one another.",
|
| 8 |
+
]
|
| 9 |
+
|
| 10 |
+
with KaLMVLLMReranker(
|
| 11 |
+
"KaLM-Embedding/KaLM-Reranker-V1-Nano",
|
| 12 |
+
encoder_chunk_size=4,
|
| 13 |
+
query_max_length=512,
|
| 14 |
+
document_max_length=1024,
|
| 15 |
+
) as reranker:
|
| 16 |
+
print(reranker.predict([(query, document) for document in documents]))
|
| 17 |
+
print(reranker.rank(query, documents))
|
vllm_support/examples/rerank_request.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"query": "What is the capital of China?",
|
| 3 |
+
"documents": [
|
| 4 |
+
"The capital of China is Beijing.",
|
| 5 |
+
"Gravity attracts bodies toward one another."
|
| 6 |
+
],
|
| 7 |
+
"instruction": "Given a query, retrieve documents that answer the query.",
|
| 8 |
+
"top_k": null,
|
| 9 |
+
"return_margin": false
|
| 10 |
+
}
|
vllm_support/examples/sample_pairs.jsonl
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{"id":"positive","query":"What is the capital of China?","document":"The capital of China is Beijing."}
|
| 2 |
+
{"id":"negative","query":"What is the capital of China?","document":"Gravity attracts bodies toward one another."}
|
vllm_support/examples/score_request.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"pairs": [
|
| 3 |
+
{
|
| 4 |
+
"id": "positive",
|
| 5 |
+
"query": "What is the capital of China?",
|
| 6 |
+
"document": "The capital of China is Beijing."
|
| 7 |
+
},
|
| 8 |
+
{
|
| 9 |
+
"id": "negative",
|
| 10 |
+
"query": "What is the capital of China?",
|
| 11 |
+
"document": "Gravity attracts bodies toward one another."
|
| 12 |
+
}
|
| 13 |
+
],
|
| 14 |
+
"instruction": null,
|
| 15 |
+
"return_margin": false
|
| 16 |
+
}
|
vllm_support/examples/start_online_server.sh
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
|
| 4 |
+
export CUDA_VISIBLE_DEVICES="${CUDA_VISIBLE_DEVICES:-0}"
|
| 5 |
+
export VLLM_PLUGINS="${VLLM_PLUGINS:-kalm_t5gemma2}"
|
| 6 |
+
if [[ -d /lib/x86_64-linux-gnu && -d /usr/lib/x86_64-linux-gnu ]]; then
|
| 7 |
+
export LD_LIBRARY_PATH="/lib/x86_64-linux-gnu:/usr/lib/x86_64-linux-gnu${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
| 8 |
+
fi
|
| 9 |
+
|
| 10 |
+
exec kalm-vllm-serve \
|
| 11 |
+
--host "${HOST:-0.0.0.0}" \
|
| 12 |
+
--port "${PORT:-8000}" \
|
| 13 |
+
--model "${MODEL:-KaLM-Embedding/KaLM-Reranker-V1-Nano}" \
|
| 14 |
+
--query-max-length "${QUERY_MAX_LENGTH:-512}" \
|
| 15 |
+
--document-max-length "${DOCUMENT_MAX_LENGTH:-1024}" \
|
| 16 |
+
--encoder-chunk-size "${ENCODER_CHUNK_SIZE:-4}" \
|
| 17 |
+
--max-model-len "${MAX_MODEL_LEN:-2048}" \
|
| 18 |
+
--batch-size "${BATCH_SIZE:-32}" \
|
| 19 |
+
--dtype "${DTYPE:-bfloat16}" \
|
| 20 |
+
--gpu-memory-utilization "${GPU_MEMORY_UTILIZATION:-0.85}"
|
vllm_support/pyproject.toml
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["setuptools>=64"]
|
| 3 |
+
build-backend = "setuptools.build_meta"
|
| 4 |
+
|
| 5 |
+
[project]
|
| 6 |
+
name = "kalm-t5gemma2-vllm-plugin"
|
| 7 |
+
version = "0.1.0"
|
| 8 |
+
description = "vLLM 0.19.1 adapter for KaLM-Reranker-V1-Nano"
|
| 9 |
+
readme = "README.md"
|
| 10 |
+
requires-python = ">=3.12,<3.13"
|
| 11 |
+
license = {text = "Apache-2.0"}
|
| 12 |
+
dependencies = [
|
| 13 |
+
"transformers==5.6.2",
|
| 14 |
+
"vllm==0.19.1",
|
| 15 |
+
]
|
| 16 |
+
|
| 17 |
+
[project.optional-dependencies]
|
| 18 |
+
online = [
|
| 19 |
+
"fastapi>=0.136,<0.137",
|
| 20 |
+
"uvicorn>=0.46,<0.47",
|
| 21 |
+
]
|
| 22 |
+
|
| 23 |
+
[project.entry-points."vllm.general_plugins"]
|
| 24 |
+
kalm_t5gemma2 = "kalm_t5gemma2_vllm_plugin.register:register"
|
| 25 |
+
|
| 26 |
+
[project.scripts]
|
| 27 |
+
kalm-vllm-rerank = "kalm_t5gemma2_vllm_plugin.cli:main"
|
| 28 |
+
kalm-vllm-serve = "kalm_t5gemma2_vllm_plugin.server:main"
|
| 29 |
+
kalm-vllm-client = "kalm_t5gemma2_vllm_plugin.client:main"
|
| 30 |
+
|
| 31 |
+
[tool.setuptools.packages.find]
|
| 32 |
+
where = ["src"]
|
| 33 |
+
|
vllm_support/src/kalm_t5gemma2_vllm_plugin.egg-info/PKG-INFO
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
vllm_support/src/kalm_t5gemma2_vllm_plugin.egg-info/entry_points.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
kalm_t5gemma2_vllm_plugin
|
vllm_support/src/kalm_t5gemma2_vllm_plugin/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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/src/kalm_t5gemma2_vllm_plugin/cli.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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/src/kalm_t5gemma2_vllm_plugin/client.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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/src/kalm_t5gemma2_vllm_plugin/constants.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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/src/kalm_t5gemma2_vllm_plugin/modeling.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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/src/kalm_t5gemma2_vllm_plugin/modeling_score.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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/src/kalm_t5gemma2_vllm_plugin/processing.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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/src/kalm_t5gemma2_vllm_plugin/register.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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/src/kalm_t5gemma2_vllm_plugin/reranker.py
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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/src/kalm_t5gemma2_vllm_plugin/server.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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())
|