Text Ranking
sentence-transformers
Safetensors
Transformers
multilingual
t5gemma2
text2text-generation
reranker
encoder-decoder
FBNL
Retrieval
RAG
Instructions to use KaLM-Embedding/KaLM-Reranker-V1-Small with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- sentence-transformers
How to use KaLM-Embedding/KaLM-Reranker-V1-Small with sentence-transformers:
from sentence_transformers import CrossEncoder model = CrossEncoder("KaLM-Embedding/KaLM-Reranker-V1-Small") query = "Which planet is known as the Red Planet?" passages = [ "Venus is often called Earth's twin because of its similar size and proximity.", "Mars, known for its reddish appearance, is often referred to as the Red Planet.", "Jupiter, the largest planet in our solar system, has a prominent red spot.", "Saturn, famous for its rings, is sometimes mistaken for the Red Planet." ] scores = model.predict([(query, passage) for passage in passages]) print(scores) - Transformers
How to use KaLM-Embedding/KaLM-Reranker-V1-Small with Transformers:
# Load model directly from transformers import AutoProcessor, AutoModelForMultimodalLM processor = AutoProcessor.from_pretrained("KaLM-Embedding/KaLM-Reranker-V1-Small") model = AutoModelForMultimodalLM.from_pretrained("KaLM-Embedding/KaLM-Reranker-V1-Small", 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())
|