Image-Text-to-Text
Transformers
Safetensors
English
qwen3_5
decision-making
classification
calibration
routing
guardrails
agents
games
on-device
system-one
jev
image-input
conversational
Instructions to use labqoat/qev-0.8b with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use labqoat/qev-0.8b with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-text-to-text", model="labqoat/qev-0.8b") messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] pipe(text=messages)# pip install -U transformers accelerate # Load model directly from transformers import AutoProcessor, AutoModelForMultimodalLM processor = AutoProcessor.from_pretrained("labqoat/qev-0.8b") model = AutoModelForMultimodalLM.from_pretrained("labqoat/qev-0.8b", device_map="auto") messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] inputs = processor.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=256) print(processor.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use labqoat/qev-0.8b with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "labqoat/qev-0.8b" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "labqoat/qev-0.8b", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker
docker model run hf.co/labqoat/qev-0.8b
- SGLang
How to use labqoat/qev-0.8b with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "labqoat/qev-0.8b" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "labqoat/qev-0.8b", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "labqoat/qev-0.8b" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "labqoat/qev-0.8b", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }' - Docker Model Runner
How to use labqoat/qev-0.8b with Docker Model Runner:
docker model run hf.co/labqoat/qev-0.8b
qev_fast: image input on the fast CUDA engine; qev.py / qev_server.py images; card speed numbers
Browse files- README.md +40 -17
- SHA256SUMS +7 -6
- qev.py +80 -16
- qev_fast/graphs.py +60 -4
- qev_fast/model.py +22 -2
- qev_fast/runner.py +140 -4
- qev_fast/vision.py +168 -0
- qev_server.py +67 -16
README.md
CHANGED
|
@@ -71,7 +71,7 @@ every move is one forward pass of about 4 ms.
|
|
| 71 |
| memory | 1.7 GB weights in bf16, 0.8 GB in 8-bit · **1.3 GB RAM** on a Mac in 8-bit |
|
| 72 |
| also first on | JevExtra (72.0%, agent and safety skills), JevSpatial (74.2%, games and space), Rules-7 (63.7%, tied) |
|
| 73 |
| output | calibrated probabilities for `choice`, `noul` (yes / no) and `score` questions, up to 255 options each |
|
| 74 |
-
| images | zero-shot image input
|
| 75 |
| formats | PyTorch + fast CUDA engine (this repo), [MLX](https://huggingface.co/labqoat/qev-0.8b-MLX), [GGUF](https://huggingface.co/labqoat/qev-0.8b-GGUF) (also served by stock `llama-server -hf labqoat/qev-0.8b-GGUF:Q8_0` at `/v1/systemone`) |
|
| 76 |
| license | Apache-2.0 |
|
| 77 |
|
|
@@ -275,11 +275,24 @@ MPS or CPU.
|
|
| 275 |
|
| 276 |
## Images
|
| 277 |
|
| 278 |
-
qev can also read images
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
requests, 107
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
|
| 284 |
```python
|
| 285 |
from qev_image import QEVImage # qev_image.py in this repo (transformers; needs pillow and torchvision)
|
|
@@ -295,11 +308,13 @@ m.decide_batch([(state, questions, images), ...]) # images are optional per r
|
|
| 295 |
|
| 296 |
An image the state does not mention is appended at the end of the state. Several `<image:...>` markers in one state
|
| 297 |
work. Each image is resized (aspect kept) to at most `max_pixels`, 448×448 by default, which costs **at most 196
|
| 298 |
-
tokens per image** (224×224: 49; 640×640: 400 with `max_pixels=640*640`
|
| 299 |
-
input and image tokens of each request
|
|
|
|
| 300 |
|
| 301 |
-
**HTTP.**
|
| 302 |
-
|
|
|
|
| 303 |
|
| 304 |
```sh
|
| 305 |
python qev_image_server.py --model labqoat/qev-0.8b --port 8018
|
|
@@ -309,9 +324,17 @@ curl http://127.0.0.1:8018/v1/systemone -H 'Content-Type: application/json' -d '
|
|
| 309 |
"questions": {"error": {"type": "noul", "instructions": "Does the screenshot show an error dialog?"}}}'
|
| 310 |
```
|
| 311 |
|
| 312 |
-
**Speed.**
|
| 313 |
-
|
| 314 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
|
| 316 |
### JevVision v0 (zero-shot)
|
| 317 |
|
|
@@ -561,7 +584,7 @@ use of the weights is for the user to assess.
|
|
| 561 |
no images in training. Boards are read close to chance (connect-four 0.38, chess 0.60, tic-tac-toe 0.59), counting
|
| 562 |
real objects is weak (CLEVR 0.40), reading a value off a chart is 0.63, and real-photo yes/no questions are
|
| 563 |
0.71–0.87. JevVision's rendered images are clean; screenshots and documents from the wild are untested. Image
|
| 564 |
-
requests run
|
| 565 |
- **Inherited behaviour.** The model inherits biases of the base model and of its data, part of which was labelled
|
| 566 |
by larger models. Do not use it as the only decision maker for consequential decisions about people (medical,
|
| 567 |
legal, credit, employment).
|
|
@@ -574,15 +597,15 @@ use of the weights is for the user to assess.
|
|
| 574 |
| `config.json`, `tokenizer.json`, `tokenizer_config.json`, `vocab.json`, `merges.txt` | the Qwen3.5 config and tokenizer |
|
| 575 |
| `qev_config.json` | temperatures, the 255 option-letter tokens, the prompt and readout description |
|
| 576 |
| `qev.py`, `requirements.txt` | the runner (backends: fast on NVIDIA GPUs, transformers elsewhere) |
|
| 577 |
-
| `qev_server.py` | an HTTP server with the System One API (`POST /v1/systemone`) |
|
| 578 |
-
| `qev_fast/`, `requirements-cuda.txt` | the fast CUDA engine `qev.py` uses by default on NVIDIA GPUs |
|
| 579 |
| `qev_image.py` | the image runner (`QEVImage`: `<image:NAME>` in the state; transformers) |
|
| 580 |
| `qev_image_server.py` | the System One HTTP server with an `"images"` field |
|
| 581 |
| `preprocessor_config.json`, `video_preprocessor_config.json` | the base model's image processor configs, unchanged |
|
| 582 |
| `SHA256SUMS` | checksums |
|
| 583 |
|
| 584 |
The checkpoint keeps the base's multimodal config and its vision tower and projector, unchanged (bit-identical to
|
| 585 |
-
Qwen3.5-0.8B-Base's). `qev.py`
|
| 586 |
|
| 587 |
**Tokenizer fix in this revision.** The model was trained with the Qwen2 pre-tokenizer pattern, which splits words
|
| 588 |
on letters only. The earlier `tokenizer.json` carried the newer Qwen3.5 pattern, which also groups combining marks.
|
|
|
|
| 71 |
| memory | 1.7 GB weights in bf16, 0.8 GB in 8-bit · **1.3 GB RAM** on a Mac in 8-bit |
|
| 72 |
| also first on | JevExtra (72.0%, agent and safety skills), JevSpatial (74.2%, games and space), Rules-7 (63.7%, tied) |
|
| 73 |
| output | calibrated probabilities for `choice`, `noul` (yes / no) and `score` questions, up to 255 options each |
|
| 74 |
+
| images | zero-shot image input (`<image:NAME>` in the state, ≤196 tokens per image) on the fast CUDA engine or with `qev_image.py`: 0.759 on JevVision v0 vs 0.651 for its base · [details](#images) |
|
| 75 |
| formats | PyTorch + fast CUDA engine (this repo), [MLX](https://huggingface.co/labqoat/qev-0.8b-MLX), [GGUF](https://huggingface.co/labqoat/qev-0.8b-GGUF) (also served by stock `llama-server -hf labqoat/qev-0.8b-GGUF:Q8_0` at `/v1/systemone`) |
|
| 76 |
| license | Apache-2.0 |
|
| 77 |
|
|
|
|
| 275 |
|
| 276 |
## Images
|
| 277 |
|
| 278 |
+
qev can also read images, with the vision tower it inherited from Qwen3.5-0.8B-Base: put `<image:NAME>` in the state
|
| 279 |
+
where the image belongs and pass the image under that name. The prompt layout, the letter readout and the per-type
|
| 280 |
+
temperatures are the same as for text, and so is the answer format. On an NVIDIA GPU, `qev.py`'s fast engine
|
| 281 |
+
(`qev_fast/`) takes images directly; `qev_image.py` runs the same model on plain transformers (any device). A text-only
|
| 282 |
+
request through `qev_image.py` gets the same answers as through `qev.py` (0 top-answer flips on 50 requests, 107
|
| 283 |
+
questions).
|
| 284 |
+
|
| 285 |
+
```python
|
| 286 |
+
from qev import QEV # the fast CUDA engine (requirements-cuda.txt); images need pillow and torchvision
|
| 287 |
+
|
| 288 |
+
m = QEV.from_pretrained("labqoat/qev-0.8b", images=True) # images=True: load the vision tower now (else on first use)
|
| 289 |
+
m.decide("A customer sent this screenshot: <image:shot>",
|
| 290 |
+
{"error": {"type": "noul", "instructions": "Does the screenshot show an error dialog?"}},
|
| 291 |
+
images={"shot": "screenshot.png"}) # a path or a PIL image
|
| 292 |
+
m.decide_batch([(state, questions, images), (state2, questions2)]) # images are optional per request
|
| 293 |
+
```
|
| 294 |
+
|
| 295 |
+
On other devices (Apple MPS, CPU, CUDA without flash-linear-attention), use `qev_image.py`:
|
| 296 |
|
| 297 |
```python
|
| 298 |
from qev_image import QEVImage # qev_image.py in this repo (transformers; needs pillow and torchvision)
|
|
|
|
| 308 |
|
| 309 |
An image the state does not mention is appended at the end of the state. Several `<image:...>` markers in one state
|
| 310 |
work. Each image is resized (aspect kept) to at most `max_pixels`, 448×448 by default, which costs **at most 196
|
| 311 |
+
tokens per image** (224×224: 49; 640×640: 400 with `max_pixels=640*640`; `QEV.from_pretrained(..., max_pixels=...)`
|
| 312 |
+
on the fast engine). `decide_batch(..., usage=True)` reports the input and image tokens of each request (on the fast
|
| 313 |
+
engine, the image tokens are in `last_image_tokens`).
|
| 314 |
|
| 315 |
+
**HTTP.** On the fast engine, `qev_server.py` accepts an `"images"` field, `{name: base64 PNG or JPEG}`, and the
|
| 316 |
+
answer adds `usage.image_tokens` (requests without images are unchanged). `qev_image_server.py` serves the same API
|
| 317 |
+
on the transformers path:
|
| 318 |
|
| 319 |
```sh
|
| 320 |
python qev_image_server.py --model labqoat/qev-0.8b --port 8018
|
|
|
|
| 324 |
"questions": {"error": {"type": "noul", "instructions": "Does the screenshot show an error dialog?"}}}'
|
| 325 |
```
|
| 326 |
|
| 327 |
+
**Speed.** On an NVIDIA GPU, `qev.py`'s fast engine runs images itself: the vision tower once per distinct image, the
|
| 328 |
+
image embeddings in the packed forward with 3-D rotary positions, CUDA graphs for small calls. On one L40S at batch 1,
|
| 329 |
+
a request with one image takes about 18–32 ms (224×224: 18 ms, 448×448: 20 ms, 768×768: 32 ms; median of 15), and
|
| 330 |
+
5–18 ms when the same image comes again (image embeddings are cached). The plain transformers path (`qev_image.py`)
|
| 331 |
+
takes about 61–69 ms. A short text request still takes 3.5 ms. Over the 1,306 benchmark requests below, in batches of
|
| 332 |
+
24, the fast engine answered about 73 requests/s and transformers about 59 requests/s in the same run (42 in our
|
| 333 |
+
earlier measurement). The first pass in a new process was slower, about 35 requests/s, because GPU kernels are
|
| 334 |
+
compiled once for new batch shapes. The fast engine's answers match `qev_image.py` up to bf16 round-off. On
|
| 335 |
+
JevVision v0 it scores 0.757 pooled accuracy and 0.115 ECE, against 0.758 and 0.113. The largest probability
|
| 336 |
+
difference is 0.05, about the same as between `qev_image.py`'s own batch-1 and batch-24 answers. Text requests are
|
| 337 |
+
unchanged, with the same probabilities bit for bit and the same speed.
|
| 338 |
|
| 339 |
### JevVision v0 (zero-shot)
|
| 340 |
|
|
|
|
| 584 |
no images in training. Boards are read close to chance (connect-four 0.38, chess 0.60, tic-tac-toe 0.59), counting
|
| 585 |
real objects is weak (CLEVR 0.40), reading a value off a chart is 0.63, and real-photo yes/no questions are
|
| 586 |
0.71–0.87. JevVision's rendered images are clean; screenshots and documents from the wild are untested. Image
|
| 587 |
+
requests run on the fast CUDA engine (`qev.py`) and the transformers path (`qev_image.py`), not on MLX or GGUF.
|
| 588 |
- **Inherited behaviour.** The model inherits biases of the base model and of its data, part of which was labelled
|
| 589 |
by larger models. Do not use it as the only decision maker for consequential decisions about people (medical,
|
| 590 |
legal, credit, employment).
|
|
|
|
| 597 |
| `config.json`, `tokenizer.json`, `tokenizer_config.json`, `vocab.json`, `merges.txt` | the Qwen3.5 config and tokenizer |
|
| 598 |
| `qev_config.json` | temperatures, the 255 option-letter tokens, the prompt and readout description |
|
| 599 |
| `qev.py`, `requirements.txt` | the runner (backends: fast on NVIDIA GPUs, transformers elsewhere) |
|
| 600 |
+
| `qev_server.py` | an HTTP server with the System One API (`POST /v1/systemone`; `"images"` on the fast engine) |
|
| 601 |
+
| `qev_fast/`, `requirements-cuda.txt` | the fast CUDA engine `qev.py` uses by default on NVIDIA GPUs (text and images) |
|
| 602 |
| `qev_image.py` | the image runner (`QEVImage`: `<image:NAME>` in the state; transformers) |
|
| 603 |
| `qev_image_server.py` | the System One HTTP server with an `"images"` field |
|
| 604 |
| `preprocessor_config.json`, `video_preprocessor_config.json` | the base model's image processor configs, unchanged |
|
| 605 |
| `SHA256SUMS` | checksums |
|
| 606 |
|
| 607 |
The checkpoint keeps the base's multimodal config and its vision tower and projector, unchanged (bit-identical to
|
| 608 |
+
Qwen3.5-0.8B-Base's). `qev.py`'s fast engine and `qev_image.py` use them for images; text requests do not.
|
| 609 |
|
| 610 |
**Tokenizer fix in this revision.** The model was trained with the Qwen2 pre-tokenizer pattern, which splits words
|
| 611 |
on letters only. The earlier `tokenizer.json` carried the newer Qwen3.5 pattern, which also groups combining marks.
|
SHA256SUMS
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
|
| 2 |
d9ab6f1698a46bbf04895d171fa0666816745e0d7209b9493d44d3df0caa1b5f config.json
|
| 3 |
aff0dc4549575e81bc423fc4d6ef963eb938f1a0330a141da15fa6211e2c5fe2 figures/accuracy_vs_size.png
|
| 4 |
95abf758a17186f4a04ce6ae4ebbf499a4bc68c67c4673c00b856ab85ee22567 figures/footprint_speed.png
|
|
@@ -11,19 +11,20 @@ f069d8b61331c9e0d845f070954770c4c011c42defb996eb01f7ff559770c724 figures/header
|
|
| 11 |
a9d356d7bdf1ef4949e3e748e95b8e10ad9d4e2e838eddc38a0a7b6b94d1db8d merges.txt
|
| 12 |
d33a719c0eedc34a530879e3840ddf0a0769b5060a987aed24e84e1170b8beb5 model.safetensors
|
| 13 |
27225450ac9c6529872ee1924fcb0962ff5634834f817040f444118116f4e516 preprocessor_config.json
|
| 14 |
-
|
| 15 |
5260608d19e1d02c7a59d7ee0351ab4a5d57cf40154132e3122eb9b147f6adb7 qev_config.json
|
| 16 |
13de61af9869524aedf9920ea0a0cc0cae4b5f05eceb0d1383e415f3077e7d38 qev_fast/__init__.py
|
| 17 |
42f113f0e51b4c94fdc60c753e7cde9bf3a45ec3a647cc1e545eaf9381a91052 qev_fast/encoder.py
|
| 18 |
ed65a44a68aefcc6397068db52274433cd372a2c5fff6b1f7f901ef543429941 qev_fast/engine.py
|
| 19 |
-
|
| 20 |
-
|
| 21 |
e9a6ef607a94a11f659f4d57fa1588c8b99b5288535ed9fb7db0a144fd817c55 qev_fast/prefix.py
|
| 22 |
-
|
| 23 |
9a7e7a159b4b6c9c1755ccbaf6c11b433bac78f8ec67474833e61821d77999ef qev_fast/systemone.py
|
|
|
|
| 24 |
e8d061404d312ca2cfbec5db8d5adbacb9fd2888e20997833f8080ed317e4184 qev_image.py
|
| 25 |
1cfc5fbdc1b4317bd2df8f2e965ee466f95a33af5db8c949d157f86c93996d4e qev_image_server.py
|
| 26 |
-
|
| 27 |
d7f47d96a76f395a93bd9c04c78e5193d7390c162c86b9ea8ce7010ec78b7ac7 requirements-cuda.txt
|
| 28 |
b60d5907148b1d4c9191a02b3910c485852af951a529527ebc1b30089939500f requirements.txt
|
| 29 |
466f27fefa1f44a7bb82cb1b42163c761e01dc6f7f51fc4f3d0a499a5a9b4249 tokenizer.json
|
|
|
|
| 1 |
+
9b2e8ee98eae68faabd10a78ca8930efa2ac735d12a42c1c410ee056742ce07a README.md
|
| 2 |
d9ab6f1698a46bbf04895d171fa0666816745e0d7209b9493d44d3df0caa1b5f config.json
|
| 3 |
aff0dc4549575e81bc423fc4d6ef963eb938f1a0330a141da15fa6211e2c5fe2 figures/accuracy_vs_size.png
|
| 4 |
95abf758a17186f4a04ce6ae4ebbf499a4bc68c67c4673c00b856ab85ee22567 figures/footprint_speed.png
|
|
|
|
| 11 |
a9d356d7bdf1ef4949e3e748e95b8e10ad9d4e2e838eddc38a0a7b6b94d1db8d merges.txt
|
| 12 |
d33a719c0eedc34a530879e3840ddf0a0769b5060a987aed24e84e1170b8beb5 model.safetensors
|
| 13 |
27225450ac9c6529872ee1924fcb0962ff5634834f817040f444118116f4e516 preprocessor_config.json
|
| 14 |
+
080cba4669a1caa43e0f94ff5943c1bc00328770c96b1c394f2e8a6f64ea4798 qev.py
|
| 15 |
5260608d19e1d02c7a59d7ee0351ab4a5d57cf40154132e3122eb9b147f6adb7 qev_config.json
|
| 16 |
13de61af9869524aedf9920ea0a0cc0cae4b5f05eceb0d1383e415f3077e7d38 qev_fast/__init__.py
|
| 17 |
42f113f0e51b4c94fdc60c753e7cde9bf3a45ec3a647cc1e545eaf9381a91052 qev_fast/encoder.py
|
| 18 |
ed65a44a68aefcc6397068db52274433cd372a2c5fff6b1f7f901ef543429941 qev_fast/engine.py
|
| 19 |
+
6bf88c5ce2089fbd603eef82680f3ede24acd8a949887e00c37fdb7e775b0479 qev_fast/graphs.py
|
| 20 |
+
0986f7be0a5049ffdb0ad49b55ab7d84d2f7916b0a4c79d0d1f82c6ce6b265c3 qev_fast/model.py
|
| 21 |
e9a6ef607a94a11f659f4d57fa1588c8b99b5288535ed9fb7db0a144fd817c55 qev_fast/prefix.py
|
| 22 |
+
c849a299772c7186b5b30feb9db777ef12673271e739051ce0968f6bbc741b27 qev_fast/runner.py
|
| 23 |
9a7e7a159b4b6c9c1755ccbaf6c11b433bac78f8ec67474833e61821d77999ef qev_fast/systemone.py
|
| 24 |
+
53c44b0101b889464a78cf7b8c8bfe518765610f39478df1b7a3e8f680cb02e4 qev_fast/vision.py
|
| 25 |
e8d061404d312ca2cfbec5db8d5adbacb9fd2888e20997833f8080ed317e4184 qev_image.py
|
| 26 |
1cfc5fbdc1b4317bd2df8f2e965ee466f95a33af5db8c949d157f86c93996d4e qev_image_server.py
|
| 27 |
+
01239024d6bce903ae16a8bda08a56b6ecbf43d630b9bd590fb1a73e9c11e54c qev_server.py
|
| 28 |
d7f47d96a76f395a93bd9c04c78e5193d7390c162c86b9ea8ce7010ec78b7ac7 requirements-cuda.txt
|
| 29 |
b60d5907148b1d4c9191a02b3910c485852af951a529527ebc1b30089939500f requirements.txt
|
| 30 |
466f27fefa1f44a7bb82cb1b42163c761e01dc6f7f51fc4f3d0a499a5a9b4249 tokenizer.json
|
qev.py
CHANGED
|
@@ -8,6 +8,11 @@
|
|
| 8 |
"urgent": {"type": "noul", "instructions": "Is this urgent?"}})
|
| 9 |
m.decide_batch([(state, questions), ...])
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
python qev.py --model labqoat/qev-0.8b --state '{"ticket": "..."}' --questions '{"urgent": {"type": "noul", "instructions": "Is this urgent?"}}'
|
| 12 |
|
| 13 |
How a request is answered (no text is generated):
|
|
@@ -24,8 +29,13 @@ Backends: "fast" (the default on CUDA when flash-linear-attention is installed:
|
|
| 24 |
with CUDA graphs and a shared-prefix per-question path; pip install -r requirements-cuda.txt) or "transformers"
|
| 25 |
(plain transformers forward; CPU, Apple MPS or CUDA). backend="auto" picks fast when it can.
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
Requires: torch, transformers (Qwen3.5 support), decider-ai==1.9.0 (Apache-2.0; prompt layout and answer format);
|
| 28 |
-
for the fast backend also flash-linear-attention (requirements-cuda.txt).
|
| 29 |
"""
|
| 30 |
|
| 31 |
from __future__ import annotations
|
|
@@ -196,6 +206,7 @@ class QEV:
|
|
| 196 |
self.fast = fast # a qev_fast.FastQEV (backend "fast"), else the transformers forward below
|
| 197 |
self.backend = "fast" if fast is not None else "transformers"
|
| 198 |
self.temps = dict(config["temperatures"])
|
|
|
|
| 199 |
if fast is not None:
|
| 200 |
return
|
| 201 |
letters = config.get("letter_token_ids") or letter_ids(tok)
|
|
@@ -206,11 +217,13 @@ class QEV:
|
|
| 206 |
@classmethod
|
| 207 |
def from_pretrained(cls, path: str = "labqoat/qev-0.8b", device: str | None = None, dtype=None,
|
| 208 |
config: str | dict | None = None, revision: str | None = None, backend: str = "auto",
|
| 209 |
-
precapture: bool = True
|
|
|
|
| 210 |
"""path: a local directory or a Hugging Face repo id. backend: "auto" (fast on CUDA with
|
| 211 |
flash-linear-attention, else transformers), "fast" or "transformers". dtype (transformers backend): bf16 on
|
| 212 |
CUDA / MPS, fp32 on CPU by default. precapture (fast): capture the common CUDA graphs now, the rest in the
|
| 213 |
-
background.
|
|
|
|
| 214 |
import sys
|
| 215 |
|
| 216 |
import torch
|
|
@@ -251,8 +264,11 @@ class QEV:
|
|
| 251 |
from qev_fast import FastQEV
|
| 252 |
|
| 253 |
fast = FastQEV(path)
|
|
|
|
|
|
|
|
|
|
| 254 |
if precapture:
|
| 255 |
-
fast.precapture(background=True)
|
| 256 |
return cls(None, fast.tok, cfg, torch.device("cuda"), torch.bfloat16, fast)
|
| 257 |
tok = use_training_pretokenizer(AutoTokenizer.from_pretrained(path))
|
| 258 |
model = AutoModelForCausalLM.from_pretrained(path, dtype=dtype).to(dev).eval()
|
|
@@ -292,8 +308,11 @@ class QEV:
|
|
| 292 |
return [encode(self.tok, state, questions, mo, ms)]
|
| 293 |
|
| 294 |
def _fast_probs(self, requests: list, per_question: bool) -> list:
|
| 295 |
-
"""Fast backend: [(state, questions)] -> per request [(row encoding, tempered probabilities)]."""
|
| 296 |
f = self.fast
|
|
|
|
|
|
|
|
|
|
| 297 |
if per_question:
|
| 298 |
res = []
|
| 299 |
for s, q in requests:
|
|
@@ -316,50 +335,91 @@ class QEV:
|
|
| 316 |
encs.append(e)
|
| 317 |
return [[(e, tempered_from_slot_probs(e, p, self.temps))] for e, p in zip(encs, f.slot_probs(encs), strict=True)]
|
| 318 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
def distributions_batch(self, requests: list, per_question: bool = False) -> list:
|
| 320 |
-
"""[(state, questions)] -> per request {question id: {option name: probability}} (tempered,
|
|
|
|
|
|
|
| 321 |
if self.fast is not None:
|
| 322 |
out = []
|
| 323 |
-
for rs in self.
|
| 324 |
d = {}
|
| 325 |
for r, p in rs:
|
| 326 |
d.update(flat(r, p))
|
| 327 |
out.append(d)
|
| 328 |
return out
|
| 329 |
rows, owner = [], []
|
| 330 |
-
for i, (s, q) in enumerate(requests):
|
| 331 |
for r in self._rows(s, q, per_question):
|
| 332 |
rows.append(r)
|
| 333 |
owner.append(i)
|
|
|
|
| 334 |
res = [{} for _ in requests]
|
| 335 |
for i, r, lg in zip(owner, rows, self._logits(rows), strict=True):
|
| 336 |
res[i].update(flat(r, probabilities(r, lg, self.temps)))
|
| 337 |
return res
|
| 338 |
|
| 339 |
def decide_batch(self, requests: list, per_question: bool = False, usage: bool = False) -> list:
|
| 340 |
-
"""[(state, questions)] -> per request {question id: answer} (decider's /v1/systemone answer
|
| 341 |
-
usage=True: per request (answers, input tokens
|
|
|
|
|
|
|
| 342 |
if self.fast is not None:
|
| 343 |
out = []
|
| 344 |
-
for rs in self.
|
| 345 |
d = {}
|
| 346 |
for r, p in rs:
|
| 347 |
d.update(answers(r, p))
|
| 348 |
out.append((d, sum(len(r["ids"]) for r, _ in rs)) if usage else d)
|
| 349 |
return out
|
| 350 |
rows, owner = [], []
|
| 351 |
-
for i, (s, q) in enumerate(requests):
|
| 352 |
for r in self._rows(s, q, per_question):
|
| 353 |
rows.append(r)
|
| 354 |
owner.append(i)
|
|
|
|
| 355 |
res, toks = [{} for _ in requests], [0] * len(requests)
|
| 356 |
for i, r, lg in zip(owner, rows, self._logits(rows), strict=True):
|
| 357 |
res[i].update(answers(r, probabilities(r, lg, self.temps)))
|
| 358 |
toks[i] += len(r["ids"])
|
| 359 |
return list(zip(res, toks, strict=True)) if usage else res
|
| 360 |
|
| 361 |
-
def decide(self, state, questions: dict, per_question: bool = False) -> dict:
|
| 362 |
-
|
|
|
|
|
|
|
| 363 |
|
| 364 |
|
| 365 |
def main():
|
|
@@ -371,8 +431,11 @@ def main():
|
|
| 371 |
ap.add_argument("--per-question", action="store_true", help="one prompt row per question")
|
| 372 |
ap.add_argument("--device", default=None)
|
| 373 |
ap.add_argument("--backend", default="auto", choices=["auto", "fast", "transformers"])
|
|
|
|
|
|
|
| 374 |
a = ap.parse_args()
|
| 375 |
-
m = QEV.from_pretrained(a.model, device=a.device, backend=a.backend)
|
|
|
|
| 376 |
if a.requests:
|
| 377 |
reqs = [json.loads(line) for line in open(a.requests) if line.strip()]
|
| 378 |
for r in m.decide_batch([(r["state"], r["questions"]) for r in reqs], a.per_question):
|
|
@@ -384,7 +447,8 @@ def main():
|
|
| 384 |
state = json.loads(a.state)
|
| 385 |
except json.JSONDecodeError:
|
| 386 |
state = a.state
|
| 387 |
-
print(json.dumps(m.decide(state, json.loads(a.questions), a.per_question), indent=1,
|
|
|
|
| 388 |
|
| 389 |
|
| 390 |
if __name__ == "__main__":
|
|
|
|
| 8 |
"urgent": {"type": "noul", "instructions": "Is this urgent?"}})
|
| 9 |
m.decide_batch([(state, questions), ...])
|
| 10 |
|
| 11 |
+
# images (fast CUDA backend; pillow + torchvision): "<image:NAME>" in the state, or appended at the end
|
| 12 |
+
m.decide("Customer screenshot: <image:shot>", {"err": {"type": "noul", "instructions": "Is an error shown?"}},
|
| 13 |
+
images={"shot": "screenshot.png"}) # PIL image or path
|
| 14 |
+
m.decide_batch([(state, questions, {"shot": img}), (state2, questions2)])
|
| 15 |
+
|
| 16 |
python qev.py --model labqoat/qev-0.8b --state '{"ticket": "..."}' --questions '{"urgent": {"type": "noul", "instructions": "Is this urgent?"}}'
|
| 17 |
|
| 18 |
How a request is answered (no text is generated):
|
|
|
|
| 29 |
with CUDA graphs and a shared-prefix per-question path; pip install -r requirements-cuda.txt) or "transformers"
|
| 30 |
(plain transformers forward; CPU, Apple MPS or CUDA). backend="auto" picks fast when it can.
|
| 31 |
|
| 32 |
+
Images: the fast backend reads images as qev_image.QEVImage does (same resize to at most max_pixels, default 448*448
|
| 33 |
+
= 196 image tokens, same placeholders and prompt layout, same readout and temperatures): the checkpoint's vision tower
|
| 34 |
+
once per distinct image, its embeddings at the image placeholders, 3-D M-RoPE positions. The transformers backend of
|
| 35 |
+
this file is text-only (use qev_image.py there).
|
| 36 |
+
|
| 37 |
Requires: torch, transformers (Qwen3.5 support), decider-ai==1.9.0 (Apache-2.0; prompt layout and answer format);
|
| 38 |
+
for the fast backend also flash-linear-attention (requirements-cuda.txt); for images pillow and torchvision.
|
| 39 |
"""
|
| 40 |
|
| 41 |
from __future__ import annotations
|
|
|
|
| 206 |
self.fast = fast # a qev_fast.FastQEV (backend "fast"), else the transformers forward below
|
| 207 |
self.backend = "fast" if fast is not None else "transformers"
|
| 208 |
self.temps = dict(config["temperatures"])
|
| 209 |
+
self.last_image_tokens: list = [] # per request of the last decide_batch / distributions_batch call
|
| 210 |
if fast is not None:
|
| 211 |
return
|
| 212 |
letters = config.get("letter_token_ids") or letter_ids(tok)
|
|
|
|
| 217 |
@classmethod
|
| 218 |
def from_pretrained(cls, path: str = "labqoat/qev-0.8b", device: str | None = None, dtype=None,
|
| 219 |
config: str | dict | None = None, revision: str | None = None, backend: str = "auto",
|
| 220 |
+
precapture: bool = True, images: bool = False, max_pixels: int = 448 * 448,
|
| 221 |
+
min_pixels: int = 64 * 64) -> "QEV":
|
| 222 |
"""path: a local directory or a Hugging Face repo id. backend: "auto" (fast on CUDA with
|
| 223 |
flash-linear-attention, else transformers), "fast" or "transformers". dtype (transformers backend): bf16 on
|
| 224 |
CUDA / MPS, fp32 on CPU by default. precapture (fast): capture the common CUDA graphs now, the rest in the
|
| 225 |
+
background. images (fast): load the vision tower now (else on the first request with images) and capture
|
| 226 |
+
the image graphs with the others; max_pixels / min_pixels bound the resized image (as qev_image.py)."""
|
| 227 |
import sys
|
| 228 |
|
| 229 |
import torch
|
|
|
|
| 264 |
from qev_fast import FastQEV
|
| 265 |
|
| 266 |
fast = FastQEV(path)
|
| 267 |
+
fast.max_pixels, fast.min_pixels = max_pixels, min_pixels
|
| 268 |
+
if images:
|
| 269 |
+
fast.enable_images(max_pixels, min_pixels)
|
| 270 |
if precapture:
|
| 271 |
+
fast.precapture(background=True, images=images)
|
| 272 |
return cls(None, fast.tok, cfg, torch.device("cuda"), torch.bfloat16, fast)
|
| 273 |
tok = use_training_pretokenizer(AutoTokenizer.from_pretrained(path))
|
| 274 |
model = AutoModelForCausalLM.from_pretrained(path, dtype=dtype).to(dev).eval()
|
|
|
|
| 308 |
return [encode(self.tok, state, questions, mo, ms)]
|
| 309 |
|
| 310 |
def _fast_probs(self, requests: list, per_question: bool) -> list:
|
| 311 |
+
"""Fast backend: [(state, questions[, images])] -> per request [(row encoding, tempered probabilities)]."""
|
| 312 |
f = self.fast
|
| 313 |
+
if any(len(r) > 2 and r[2] for r in requests):
|
| 314 |
+
return self._fast_image_probs(requests, per_question)
|
| 315 |
+
requests = [tuple(r[:2]) for r in requests]
|
| 316 |
if per_question:
|
| 317 |
res = []
|
| 318 |
for s, q in requests:
|
|
|
|
| 335 |
encs.append(e)
|
| 336 |
return [[(e, tempered_from_slot_probs(e, p, self.temps))] for e, p in zip(encs, f.slot_probs(encs), strict=True)]
|
| 337 |
|
| 338 |
+
def _fast_image_probs(self, requests: list, per_question: bool) -> list:
|
| 339 |
+
"""Fast backend, a call with images: rows from FastQEV.encode_requests (image placeholders expanded)."""
|
| 340 |
+
f = self.fast
|
| 341 |
+
for r in requests:
|
| 342 |
+
if not isinstance(r[1], dict) or not r[1]:
|
| 343 |
+
raise ValueError("questions: a non-empty {id: question} object")
|
| 344 |
+
if per_question:
|
| 345 |
+
res = []
|
| 346 |
+
for r in requests:
|
| 347 |
+
s, q, imgs = r[0], r[1], (r[2] if len(r) > 2 else None)
|
| 348 |
+
rows = []
|
| 349 |
+
for e in f.encode_requests([(s, {k: v}, imgs) for k, v in q.items()] if len(q) > 1 else [(s, q, imgs)]):
|
| 350 |
+
if isinstance(e, Exception):
|
| 351 |
+
raise e
|
| 352 |
+
rows.append(e)
|
| 353 |
+
probs = f.per_question_probs(rows) if len(rows) > 1 else f.slot_probs(rows)
|
| 354 |
+
res.append([(r_, tempered_from_slot_probs(r_, p, self.temps)) for r_, p in zip(rows, probs, strict=True)])
|
| 355 |
+
return res
|
| 356 |
+
encs = f.encode_requests(requests)
|
| 357 |
+
for e in encs:
|
| 358 |
+
if isinstance(e, Exception):
|
| 359 |
+
raise e
|
| 360 |
+
return [[(e, tempered_from_slot_probs(e, p, self.temps))] for e, p in zip(encs, f.slot_probs(encs), strict=True)]
|
| 361 |
+
|
| 362 |
+
def _check_images(self, requests: list):
|
| 363 |
+
if self.fast is None and any(len(r) > 2 and r[2] for r in requests):
|
| 364 |
+
raise ValueError("images need the fast CUDA backend of qev.py; on other devices use qev_image.py")
|
| 365 |
+
|
| 366 |
+
def _fast_results(self, requests: list, per_question: bool) -> list:
|
| 367 |
+
res = self._fast_probs(requests, per_question)
|
| 368 |
+
self.last_image_tokens = [sum(r.get("image_tokens", 0) for r, _ in rs) for rs in res]
|
| 369 |
+
return res
|
| 370 |
+
|
| 371 |
def distributions_batch(self, requests: list, per_question: bool = False) -> list:
|
| 372 |
+
"""[(state, questions[, images])] -> per request {question id: {option name: probability}} (tempered,
|
| 373 |
+
unrounded). images: {name: PIL image or path} (fast backend)."""
|
| 374 |
+
self._check_images(requests)
|
| 375 |
if self.fast is not None:
|
| 376 |
out = []
|
| 377 |
+
for rs in self._fast_results(requests, per_question):
|
| 378 |
d = {}
|
| 379 |
for r, p in rs:
|
| 380 |
d.update(flat(r, p))
|
| 381 |
out.append(d)
|
| 382 |
return out
|
| 383 |
rows, owner = [], []
|
| 384 |
+
for i, (s, q, *_) in enumerate(requests):
|
| 385 |
for r in self._rows(s, q, per_question):
|
| 386 |
rows.append(r)
|
| 387 |
owner.append(i)
|
| 388 |
+
self.last_image_tokens = [0] * len(requests)
|
| 389 |
res = [{} for _ in requests]
|
| 390 |
for i, r, lg in zip(owner, rows, self._logits(rows), strict=True):
|
| 391 |
res[i].update(flat(r, probabilities(r, lg, self.temps)))
|
| 392 |
return res
|
| 393 |
|
| 394 |
def decide_batch(self, requests: list, per_question: bool = False, usage: bool = False) -> list:
|
| 395 |
+
"""[(state, questions[, images])] -> per request {question id: answer} (decider's /v1/systemone answer
|
| 396 |
+
format). images: {name: PIL image or path} (fast backend). usage=True: per request (answers, input tokens
|
| 397 |
+
read, image tokens included); the image tokens per request are in last_image_tokens."""
|
| 398 |
+
self._check_images(requests)
|
| 399 |
if self.fast is not None:
|
| 400 |
out = []
|
| 401 |
+
for rs in self._fast_results(requests, per_question):
|
| 402 |
d = {}
|
| 403 |
for r, p in rs:
|
| 404 |
d.update(answers(r, p))
|
| 405 |
out.append((d, sum(len(r["ids"]) for r, _ in rs)) if usage else d)
|
| 406 |
return out
|
| 407 |
rows, owner = [], []
|
| 408 |
+
for i, (s, q, *_) in enumerate(requests):
|
| 409 |
for r in self._rows(s, q, per_question):
|
| 410 |
rows.append(r)
|
| 411 |
owner.append(i)
|
| 412 |
+
self.last_image_tokens = [0] * len(requests)
|
| 413 |
res, toks = [{} for _ in requests], [0] * len(requests)
|
| 414 |
for i, r, lg in zip(owner, rows, self._logits(rows), strict=True):
|
| 415 |
res[i].update(answers(r, probabilities(r, lg, self.temps)))
|
| 416 |
toks[i] += len(r["ids"])
|
| 417 |
return list(zip(res, toks, strict=True)) if usage else res
|
| 418 |
|
| 419 |
+
def decide(self, state, questions: dict, per_question: bool = False, images: dict | None = None) -> dict:
|
| 420 |
+
"""images: {name: PIL image or path} (fast backend); "<image:NAME>" in the state places one, the others go at
|
| 421 |
+
the end of the context."""
|
| 422 |
+
return self.decide_batch([(state, questions, images) if images else (state, questions)], per_question)[0]
|
| 423 |
|
| 424 |
|
| 425 |
def main():
|
|
|
|
| 431 |
ap.add_argument("--per-question", action="store_true", help="one prompt row per question")
|
| 432 |
ap.add_argument("--device", default=None)
|
| 433 |
ap.add_argument("--backend", default="auto", choices=["auto", "fast", "transformers"])
|
| 434 |
+
ap.add_argument("--image", action="append", default=[], help="NAME=PATH (repeatable; fast backend)")
|
| 435 |
+
ap.add_argument("--max-pixels", type=int, default=448 * 448)
|
| 436 |
a = ap.parse_args()
|
| 437 |
+
m = QEV.from_pretrained(a.model, device=a.device, backend=a.backend, max_pixels=a.max_pixels)
|
| 438 |
+
imgs = dict(x.split("=", 1) for x in a.image) or None
|
| 439 |
if a.requests:
|
| 440 |
reqs = [json.loads(line) for line in open(a.requests) if line.strip()]
|
| 441 |
for r in m.decide_batch([(r["state"], r["questions"]) for r in reqs], a.per_question):
|
|
|
|
| 447 |
state = json.loads(a.state)
|
| 448 |
except json.JSONDecodeError:
|
| 449 |
state = a.state
|
| 450 |
+
print(json.dumps(m.decide(state, json.loads(a.questions), a.per_question, images=imgs), indent=1,
|
| 451 |
+
ensure_ascii=False))
|
| 452 |
|
| 453 |
|
| 454 |
if __name__ == "__main__":
|
qev_fast/graphs.py
CHANGED
|
@@ -17,6 +17,10 @@ captured on first use, replays the whole forward:
|
|
| 17 |
- the letter logits at the answer slots, masked to each slot's options, softmax -> probabilities [rows * max_slots, L].
|
| 18 |
|
| 19 |
Rows longer than the largest bucket, more rows or more slots than the limits: the caller's eager path.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
"""
|
| 21 |
|
| 22 |
import time
|
|
@@ -60,6 +64,7 @@ class GraphSociety:
|
|
| 60 |
"uncertain": spec.get("k", 0)}[spec["kind"]] if self.E else 0
|
| 61 |
self.graphs: dict = {}
|
| 62 |
self.max_tokens = MAX_GRAPH_TOKENS
|
|
|
|
| 63 |
self.pool = torch.cuda.graph_pool_handle()
|
| 64 |
self.stats = {"graph": 0, "captures": 0, "capture_s": 0.0, "capture_failed": []}
|
| 65 |
|
|
@@ -128,13 +133,18 @@ class GraphSociety:
|
|
| 128 |
def _forward(self, st):
|
| 129 |
fs, fm = self.fs, self.fs.fm
|
| 130 |
n, T, b = st["n"], st["T"], st["b"]
|
| 131 |
-
|
|
|
|
|
|
|
|
|
|
| 132 |
fm.m_tok = None
|
| 133 |
fm.lora_gather = None
|
| 134 |
if st.get("h6") is not None: # the hidden state at router_layer given (prefix-shared per-question rows)
|
| 135 |
h = st["h6"]
|
| 136 |
else:
|
| 137 |
h = fm.embed(b.ids)
|
|
|
|
|
|
|
| 138 |
for i in range(fs.router_layer):
|
| 139 |
h = fm.layer(i, h, b, rope)
|
| 140 |
mask = st["mask"] # [n, T] float32, 1 on real tokens
|
|
@@ -167,7 +177,15 @@ class GraphSociety:
|
|
| 167 |
float("-inf"))
|
| 168 |
return torch.softmax(logits, -1)
|
| 169 |
|
| 170 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
t0 = time.perf_counter()
|
| 172 |
b = Batch([[self.pad_id] * T for _ in range(n)], [[0] * self.max_slots for _ in range(n)], "cuda")
|
| 173 |
if n > 1: # equal-length rows as a real batch: no cu_seqlens (fla's varlen kernels are not graph-safe)
|
|
@@ -176,6 +194,9 @@ class GraphSociety:
|
|
| 176 |
st = {"n": n, "T": T, "b": b, "mask": torch.ones(n, T, device=dev), "count": torch.full((n,), float(T), device=dev),
|
| 177 |
"nopts": torch.ones(n * self.max_slots, dtype=torch.long, device=dev),
|
| 178 |
"slot_row": torch.arange(n, device=dev).repeat_interleave(self.max_slots)}
|
|
|
|
|
|
|
|
|
|
| 179 |
if upper:
|
| 180 |
st["h6"] = torch.zeros(n * T, self.fs.fm.embed.weight.shape[1], device=dev, dtype=self.fs.fm.embed.weight.dtype)
|
| 181 |
s = torch.cuda.Stream()
|
|
@@ -215,6 +236,21 @@ class GraphSociety:
|
|
| 215 |
"gpu_reserved_gb": round(torch.cuda.memory_reserved() / 1e9, 2),
|
| 216 |
"failed": self.stats["capture_failed"][:3]}
|
| 217 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
def warm(self, shapes):
|
| 219 |
for n, T in shapes:
|
| 220 |
key = (self._bucket(n, self.row_buckets), self._bucket(T, self.len_buckets))
|
|
@@ -282,9 +318,10 @@ class GraphSociety:
|
|
| 282 |
"""Society-style encodings (eligible) -> per row its slot probabilities (list of [nopts] lists)."""
|
| 283 |
n = self._bucket(len(encs), self.row_buckets)
|
| 284 |
T = self._bucket(max(len(e[0]["ids"]) for e in encs), self.len_buckets)
|
| 285 |
-
|
|
|
|
| 286 |
if key not in self.graphs:
|
| 287 |
-
self.graphs[key] = self._capture(n, T)
|
| 288 |
hit = self.graphs[key]
|
| 289 |
if hit is None:
|
| 290 |
return None # not capturable: the caller's eager path
|
|
@@ -310,6 +347,25 @@ class GraphSociety:
|
|
| 310 |
st["mask"].copy_(mask, non_blocking=True)
|
| 311 |
st["count"].copy_(count, non_blocking=True)
|
| 312 |
st["nopts"].copy_(nopts.view(-1), non_blocking=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
g.replay()
|
| 314 |
self.stats["graph"] += 1
|
| 315 |
p = out.view(n, S, -1)[: len(encs)].cpu()
|
|
|
|
| 17 |
- the letter logits at the answer slots, masked to each slot's options, softmax -> probabilities [rows * max_slots, L].
|
| 18 |
|
| 19 |
Rows longer than the largest bucket, more rows or more slots than the limits: the caller's eager path.
|
| 20 |
+
|
| 21 |
+
Image rows (encodings with "img_emb", "img_rows", "pos3"; qev_fast.runner) use their own graphs, keyed ("i", rows,
|
| 22 |
+
length): the token embeddings of the image rows replaced from a static embedding buffer and the rotary tables built
|
| 23 |
+
from static 3-D M-RoPE positions. The text graphs are not changed by them.
|
| 24 |
"""
|
| 25 |
|
| 26 |
import time
|
|
|
|
| 64 |
"uncertain": spec.get("k", 0)}[spec["kind"]] if self.E else 0
|
| 65 |
self.graphs: dict = {}
|
| 66 |
self.max_tokens = MAX_GRAPH_TOKENS
|
| 67 |
+
self.img_buf = None # (embeddings [max_tokens, H], mask [max_tokens, 1], pos3 [3, max_tokens]): image graphs
|
| 68 |
self.pool = torch.cuda.graph_pool_handle()
|
| 69 |
self.stats = {"graph": 0, "captures": 0, "capture_s": 0.0, "capture_failed": []}
|
| 70 |
|
|
|
|
| 133 |
def _forward(self, st):
|
| 134 |
fs, fm = self.fs, self.fs.fm
|
| 135 |
n, T, b = st["n"], st["T"], st["b"]
|
| 136 |
+
if "pos3" in st: # image rows: 3-D positions
|
| 137 |
+
rope = M.mrope_tables(st["pos3"], fm.inv_freq, fm.embed.weight.dtype, fm.mrope_section)
|
| 138 |
+
else:
|
| 139 |
+
rope = M.rope_tables(b.pos, fm.inv_freq, fm.embed.weight.dtype)
|
| 140 |
fm.m_tok = None
|
| 141 |
fm.lora_gather = None
|
| 142 |
if st.get("h6") is not None: # the hidden state at router_layer given (prefix-shared per-question rows)
|
| 143 |
h = st["h6"]
|
| 144 |
else:
|
| 145 |
h = fm.embed(b.ids)
|
| 146 |
+
if "img_mask" in st:
|
| 147 |
+
h = torch.where(st["img_mask"], st["img_emb"], h)
|
| 148 |
for i in range(fs.router_layer):
|
| 149 |
h = fm.layer(i, h, b, rope)
|
| 150 |
mask = st["mask"] # [n, T] float32, 1 on real tokens
|
|
|
|
| 177 |
float("-inf"))
|
| 178 |
return torch.softmax(logits, -1)
|
| 179 |
|
| 180 |
+
def _image_buffers(self):
|
| 181 |
+
if self.img_buf is None:
|
| 182 |
+
w = self.fs.fm.embed.weight
|
| 183 |
+
self.img_buf = (torch.zeros(self.max_tokens, w.shape[1], device=w.device, dtype=w.dtype),
|
| 184 |
+
torch.zeros(self.max_tokens, 1, device=w.device, dtype=torch.bool),
|
| 185 |
+
torch.zeros(3, self.max_tokens, device=w.device, dtype=torch.long))
|
| 186 |
+
return self.img_buf
|
| 187 |
+
|
| 188 |
+
def _capture(self, n, T, upper: bool = False, image: bool = False):
|
| 189 |
t0 = time.perf_counter()
|
| 190 |
b = Batch([[self.pad_id] * T for _ in range(n)], [[0] * self.max_slots for _ in range(n)], "cuda")
|
| 191 |
if n > 1: # equal-length rows as a real batch: no cu_seqlens (fla's varlen kernels are not graph-safe)
|
|
|
|
| 194 |
st = {"n": n, "T": T, "b": b, "mask": torch.ones(n, T, device=dev), "count": torch.full((n,), float(T), device=dev),
|
| 195 |
"nopts": torch.ones(n * self.max_slots, dtype=torch.long, device=dev),
|
| 196 |
"slot_row": torch.arange(n, device=dev).repeat_interleave(self.max_slots)}
|
| 197 |
+
if image: # views of the shared image buffers (static addresses)
|
| 198 |
+
emb, msk, p3 = self._image_buffers()
|
| 199 |
+
st["img_emb"], st["img_mask"], st["pos3"] = emb[: n * T], msk[: n * T], p3[:, : n * T]
|
| 200 |
if upper:
|
| 201 |
st["h6"] = torch.zeros(n * T, self.fs.fm.embed.weight.shape[1], device=dev, dtype=self.fs.fm.embed.weight.dtype)
|
| 202 |
s = torch.cuda.Stream()
|
|
|
|
| 236 |
"gpu_reserved_gb": round(torch.cuda.memory_reserved() / 1e9, 2),
|
| 237 |
"failed": self.stats["capture_failed"][:3]}
|
| 238 |
|
| 239 |
+
def precapture_images(self, max_rows: int = 4, lengths=(192, 256, 320, 384, 448, 512, 640, 768, 1024),
|
| 240 |
+
lock=None) -> dict:
|
| 241 |
+
"""Capture the image graph buckets (rows <= max_rows, these length buckets)."""
|
| 242 |
+
import contextlib
|
| 243 |
+
|
| 244 |
+
t0 = time.perf_counter()
|
| 245 |
+
for T in lengths:
|
| 246 |
+
for n in self.row_buckets:
|
| 247 |
+
if n <= max_rows and n * T <= self.max_tokens and ("i", n, T) not in self.graphs:
|
| 248 |
+
with lock if lock is not None else contextlib.nullcontext():
|
| 249 |
+
self.graphs[("i", n, T)] = self._capture(n, T, image=True)
|
| 250 |
+
torch.cuda.synchronize()
|
| 251 |
+
return {"image_graphs": sum(1 for k, g in self.graphs.items() if k[0] == "i" and g is not None),
|
| 252 |
+
"s": round(time.perf_counter() - t0, 1)}
|
| 253 |
+
|
| 254 |
def warm(self, shapes):
|
| 255 |
for n, T in shapes:
|
| 256 |
key = (self._bucket(n, self.row_buckets), self._bucket(T, self.len_buckets))
|
|
|
|
| 318 |
"""Society-style encodings (eligible) -> per row its slot probabilities (list of [nopts] lists)."""
|
| 319 |
n = self._bucket(len(encs), self.row_buckets)
|
| 320 |
T = self._bucket(max(len(e[0]["ids"]) for e in encs), self.len_buckets)
|
| 321 |
+
image = any("pos3" in e[0] for e in encs)
|
| 322 |
+
key = ("i", n, T) if image else (n, T)
|
| 323 |
if key not in self.graphs:
|
| 324 |
+
self.graphs[key] = self._capture(n, T, image=image)
|
| 325 |
hit = self.graphs[key]
|
| 326 |
if hit is None:
|
| 327 |
return None # not capturable: the caller's eager path
|
|
|
|
| 347 |
st["mask"].copy_(mask, non_blocking=True)
|
| 348 |
st["count"].copy_(count, non_blocking=True)
|
| 349 |
st["nopts"].copy_(nopts.view(-1), non_blocking=True)
|
| 350 |
+
if image:
|
| 351 |
+
import numpy as np
|
| 352 |
+
|
| 353 |
+
pos3 = torch.zeros(3, n, T, dtype=torch.long)
|
| 354 |
+
rows, embs = [], []
|
| 355 |
+
for j, e in enumerate(encs):
|
| 356 |
+
it = e[0]
|
| 357 |
+
L = len(it["ids"])
|
| 358 |
+
if "pos3" in it:
|
| 359 |
+
pos3[:, j, :L] = torch.from_numpy(it["pos3"])
|
| 360 |
+
rows.append(j * T + it["img_rows"])
|
| 361 |
+
embs.append(it["img_emb"])
|
| 362 |
+
else:
|
| 363 |
+
pos3[:, j, :L] = torch.arange(L)
|
| 364 |
+
st["pos3"].copy_(pos3.view(3, -1), non_blocking=True)
|
| 365 |
+
idx = torch.from_numpy(np.concatenate(rows)).to(st["img_mask"].device, non_blocking=True)
|
| 366 |
+
st["img_mask"].zero_()
|
| 367 |
+
st["img_mask"].index_fill_(0, idx, True)
|
| 368 |
+
st["img_emb"].index_copy_(0, idx, torch.cat(embs).to(st["img_emb"].dtype))
|
| 369 |
g.replay()
|
| 370 |
self.stats["graph"] += 1
|
| 371 |
p = out.view(n, S, -1)[: len(encs)].cpu()
|
qev_fast/model.py
CHANGED
|
@@ -56,6 +56,18 @@ def rope_tables(pos, inv_freq, dtype):
|
|
| 56 |
return emb.cos().to(dtype), emb.sin().to(dtype)
|
| 57 |
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
def apply_rope(x, cos, sin):
|
| 60 |
"""x [T, H, D] with partial rotary on the first cos.shape[-1] dims (apply_rotary_pos_emb)."""
|
| 61 |
r = cos.shape[-1]
|
|
@@ -113,7 +125,9 @@ def attend(q, k, v, cu, cu_list, max_len: int, scale: float, backend: str):
|
|
| 113 |
# ---- the model -------------------------------------------------------------------------------------------------------
|
| 114 |
class Batch:
|
| 115 |
"""A packed forward's inputs on the device: ids [T], cu [N+1] int32 (None for one sequence), pos [T], slots [S]
|
| 116 |
-
(packed row of every answer slot), cu_cpu (host copy of cu), max_len.
|
|
|
|
|
|
|
| 117 |
|
| 118 |
def __init__(self, seqs: list, slots: list, device, pad_to: int | None = None, pad_id: int = 0):
|
| 119 |
"""seqs: token id lists; slots: per sequence, slot offsets in it. pad_to: one sequence right-padded to this
|
|
@@ -163,6 +177,7 @@ class FastQwen35(nn.Module):
|
|
| 163 |
self.register_buffer("letters", torch.as_tensor(letters, dtype=torch.long, device=self.embed.weight.device),
|
| 164 |
persistent=False)
|
| 165 |
self.inv_freq = core.rotary_emb.inv_freq
|
|
|
|
| 166 |
self.L = []
|
| 167 |
for i, layer in enumerate(core.layers):
|
| 168 |
d = {"in_norm": layer.input_layernorm.weight, "post_norm": layer.post_attention_layernorm.weight,
|
|
@@ -280,7 +295,12 @@ class FastQwen35(nn.Module):
|
|
| 280 |
"""-> final-norm hidden states at the slots [S, H]. ckpt: "none", "full" (recompute every layer in backward)
|
| 281 |
or "attn" (recompute only the full-attention layers: the delta-net layers keep their activations)."""
|
| 282 |
h = self.embed(b.ids)
|
| 283 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
n = len(self.L)
|
| 285 |
for i in range(n):
|
| 286 |
rows = b.slots if i == n - 1 else None
|
|
|
|
| 56 |
return emb.cos().to(dtype), emb.sin().to(dtype)
|
| 57 |
|
| 58 |
|
| 59 |
+
def mrope_tables(pos3, inv_freq, dtype, section):
|
| 60 |
+
"""cos / sin [T, rot] of 3-D positions pos3 [3, T] (t, h, w): Qwen3.5's interleaved M-RoPE (frequency j takes the
|
| 61 |
+
h position for j = 1, 4, ... < 3 section[1], the w position for j = 2, 5, ... < 3 section[2], else t; the
|
| 62 |
+
transformers apply_interleaved_mrope). Equal rows give exactly rope_tables(pos3[0])."""
|
| 63 |
+
f = pos3.float()[:, :, None] * inv_freq.float()[None, None] # [3, T, rot / 2]
|
| 64 |
+
out = f[0].clone()
|
| 65 |
+
out[:, 1: 3 * section[1]: 3] = f[1][:, 1: 3 * section[1]: 3]
|
| 66 |
+
out[:, 2: 3 * section[2]: 3] = f[2][:, 2: 3 * section[2]: 3]
|
| 67 |
+
emb = torch.cat([out, out], -1)
|
| 68 |
+
return emb.cos().to(dtype), emb.sin().to(dtype)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
def apply_rope(x, cos, sin):
|
| 72 |
"""x [T, H, D] with partial rotary on the first cos.shape[-1] dims (apply_rotary_pos_emb)."""
|
| 73 |
r = cos.shape[-1]
|
|
|
|
| 125 |
# ---- the model -------------------------------------------------------------------------------------------------------
|
| 126 |
class Batch:
|
| 127 |
"""A packed forward's inputs on the device: ids [T], cu [N+1] int32 (None for one sequence), pos [T], slots [S]
|
| 128 |
+
(packed row of every answer slot), cu_cpu (host copy of cu), max_len. Image rows (optional attributes, set by the
|
| 129 |
+
caller): img = (rows [R] long, embeddings [R, H]) replaces the token embeddings of those rows; pos3 [3, T] are the
|
| 130 |
+
M-RoPE positions (text-only batches have neither and run exactly as before)."""
|
| 131 |
|
| 132 |
def __init__(self, seqs: list, slots: list, device, pad_to: int | None = None, pad_id: int = 0):
|
| 133 |
"""seqs: token id lists; slots: per sequence, slot offsets in it. pad_to: one sequence right-padded to this
|
|
|
|
| 177 |
self.register_buffer("letters", torch.as_tensor(letters, dtype=torch.long, device=self.embed.weight.device),
|
| 178 |
persistent=False)
|
| 179 |
self.inv_freq = core.rotary_emb.inv_freq
|
| 180 |
+
self.mrope_section = list(getattr(core.rotary_emb, "mrope_section", None) or [11, 11, 10])
|
| 181 |
self.L = []
|
| 182 |
for i, layer in enumerate(core.layers):
|
| 183 |
d = {"in_norm": layer.input_layernorm.weight, "post_norm": layer.post_attention_layernorm.weight,
|
|
|
|
| 295 |
"""-> final-norm hidden states at the slots [S, H]. ckpt: "none", "full" (recompute every layer in backward)
|
| 296 |
or "attn" (recompute only the full-attention layers: the delta-net layers keep their activations)."""
|
| 297 |
h = self.embed(b.ids)
|
| 298 |
+
img = getattr(b, "img", None)
|
| 299 |
+
if img is not None: # image rows: the vision tower's embeddings instead of the placeholder token's
|
| 300 |
+
h = h.index_copy(0, img[0], img[1].to(h.dtype))
|
| 301 |
+
pos3 = getattr(b, "pos3", None)
|
| 302 |
+
rope = (rope_tables(b.pos, self.inv_freq, h.dtype) if pos3 is None
|
| 303 |
+
else mrope_tables(pos3, self.inv_freq, h.dtype, self.mrope_section))
|
| 304 |
n = len(self.L)
|
| 305 |
for i in range(n):
|
| 306 |
rows = b.slots if i == n - 1 else None
|
qev_fast/runner.py
CHANGED
|
@@ -7,6 +7,12 @@
|
|
| 7 |
state;
|
| 8 |
- precapture(): the common graph buckets now, the rest in a background thread under the GPU lock.
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
Outputs are each slot's option probabilities (softmax over its options, untempered); qev.py renormalises over the
|
| 11 |
named options, tempers per type and formats the answers.
|
| 12 |
"""
|
|
@@ -45,6 +51,8 @@ def _softmax(xs):
|
|
| 45 |
class FastQEV:
|
| 46 |
def __init__(self, path: str, attn: str | None = None, graphs: bool = True, max_batch_tokens: int = 32768):
|
| 47 |
t = time.perf_counter()
|
|
|
|
|
|
|
| 48 |
torch.backends.cuda.enable_cudnn_sdp(False)
|
| 49 |
lm, self.tok, letters = E.load_a1(path, None)
|
| 50 |
self.attn = attn or _attn_backend()
|
|
@@ -60,15 +68,18 @@ class FastQEV:
|
|
| 60 |
self.load_s = time.perf_counter() - t
|
| 61 |
|
| 62 |
# ---- warm-up ---------------------------------------------------------------------------------------------------
|
| 63 |
-
def precapture(self, background: bool = True, per_question: bool = False) -> dict:
|
| 64 |
"""Capture the common graph buckets (<= 4 rows, <= 2048 tokens) now; the rest in a background thread (or now,
|
| 65 |
-
background=False). per_question: the prefix / suffix graphs too.
|
|
|
|
| 66 |
if self.graphs is None:
|
| 67 |
return {}
|
| 68 |
with self.lock:
|
| 69 |
info = self.graphs.precapture(max_rows=4, max_len=2048)
|
| 70 |
|
| 71 |
def rest():
|
|
|
|
|
|
|
| 72 |
self.graphs.precapture(lock=self.lock)
|
| 73 |
if per_question:
|
| 74 |
with self.lock:
|
|
@@ -80,9 +91,134 @@ class FastQEV:
|
|
| 80 |
rest()
|
| 81 |
return info
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
# ---- slot probabilities -------------------------------------------------------------------------------------------
|
| 84 |
def slot_probs(self, encs: list) -> list:
|
| 85 |
-
"""Prompt rows (encoder dicts) -> per row, per slot: softmax over the slot's options (untempered).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
out = [None] * len(encs)
|
| 87 |
with self.lock, torch.no_grad():
|
| 88 |
if encs and self.graphs is not None and len(encs) <= self.graphs.row_buckets[-1]:
|
|
@@ -112,7 +248,7 @@ class FastQEV:
|
|
| 112 |
def per_question_probs(self, rows: list) -> list:
|
| 113 |
"""The per-question rows of one request: their longest common prefix computed once (exact up to bf16
|
| 114 |
round-off), else as an ordinary batch."""
|
| 115 |
-
if len(rows) > 1:
|
| 116 |
m = min(len(e["ids"]) for e in rows)
|
| 117 |
first = rows[0]["ids"]
|
| 118 |
lcp = 0
|
|
|
|
| 7 |
state;
|
| 8 |
- precapture(): the common graph buckets now, the rest in a background thread under the GPU lock.
|
| 9 |
|
| 10 |
+
- images (enable_images(), qev_fast.vision): "<image:NAME>" placeholders expanded as qev_image.QEVImage does, the
|
| 11 |
+
checkpoint's vision tower run once per distinct image of a call, its embeddings scattered into the image rows and
|
| 12 |
+
3-D M-RoPE positions; image rows run on their own CUDA graphs (a separate graph family with an embedding buffer, so
|
| 13 |
+
the text graphs and the text path are unchanged) or packed eagerly; per_question rows with images run as an
|
| 14 |
+
ordinary batch (no prefix sharing).
|
| 15 |
+
|
| 16 |
Outputs are each slot's option probabilities (softmax over its options, untempered); qev.py renormalises over the
|
| 17 |
named options, tempers per type and formats the answers.
|
| 18 |
"""
|
|
|
|
| 51 |
class FastQEV:
|
| 52 |
def __init__(self, path: str, attn: str | None = None, graphs: bool = True, max_batch_tokens: int = 32768):
|
| 53 |
t = time.perf_counter()
|
| 54 |
+
self.path = path
|
| 55 |
+
self.vision = None # enable_images() loads the vision tower
|
| 56 |
torch.backends.cuda.enable_cudnn_sdp(False)
|
| 57 |
lm, self.tok, letters = E.load_a1(path, None)
|
| 58 |
self.attn = attn or _attn_backend()
|
|
|
|
| 68 |
self.load_s = time.perf_counter() - t
|
| 69 |
|
| 70 |
# ---- warm-up ---------------------------------------------------------------------------------------------------
|
| 71 |
+
def precapture(self, background: bool = True, per_question: bool = False, images: bool = False) -> dict:
|
| 72 |
"""Capture the common graph buckets (<= 4 rows, <= 2048 tokens) now; the rest in a background thread (or now,
|
| 73 |
+
background=False). per_question: the prefix / suffix graphs too. images: the image graphs (<= 4 rows, 192 to
|
| 74 |
+
1024 tokens) too (after enable_images())."""
|
| 75 |
if self.graphs is None:
|
| 76 |
return {}
|
| 77 |
with self.lock:
|
| 78 |
info = self.graphs.precapture(max_rows=4, max_len=2048)
|
| 79 |
|
| 80 |
def rest():
|
| 81 |
+
if images and self.vision is not None:
|
| 82 |
+
self.graphs.precapture_images(lock=self.lock)
|
| 83 |
self.graphs.precapture(lock=self.lock)
|
| 84 |
if per_question:
|
| 85 |
with self.lock:
|
|
|
|
| 91 |
rest()
|
| 92 |
return info
|
| 93 |
|
| 94 |
+
# ---- images -------------------------------------------------------------------------------------------------
|
| 95 |
+
def enable_images(self, max_pixels: int = 448 * 448, min_pixels: int = 64 * 64):
|
| 96 |
+
"""Load the vision tower (once; needs pillow, torchvision and the checkpoint's preprocessor_config.json)."""
|
| 97 |
+
if self.vision is None:
|
| 98 |
+
from .vision import Vision
|
| 99 |
+
|
| 100 |
+
with self.lock:
|
| 101 |
+
self.vision = Vision(self.path)
|
| 102 |
+
self.marks = self.vision.tokens(self.tok)
|
| 103 |
+
self.max_pixels, self.min_pixels = max_pixels, min_pixels
|
| 104 |
+
return self
|
| 105 |
+
|
| 106 |
+
def encode_requests(self, requests: list) -> list:
|
| 107 |
+
"""[(state, questions[, images])] -> per request its prompt row (encoder dict; with images also "images":
|
| 108 |
+
the Prepped images in placeholder order, "image_tokens") or an exception instance."""
|
| 109 |
+
from decider.systemone import render_state
|
| 110 |
+
|
| 111 |
+
plain, pre, errs = [], {}, {}
|
| 112 |
+
for i, r in enumerate(requests):
|
| 113 |
+
s, q, *rest = r
|
| 114 |
+
imgs = rest[0] if rest else None
|
| 115 |
+
if imgs:
|
| 116 |
+
if self.vision is None:
|
| 117 |
+
self.enable_images()
|
| 118 |
+
try:
|
| 119 |
+
cache: dict = {}
|
| 120 |
+
|
| 121 |
+
def prepped_of(k, imgs=imgs, cache=cache):
|
| 122 |
+
if k not in cache:
|
| 123 |
+
cache[k] = self.vision.prep(imgs[k], self.min_pixels, self.max_pixels)
|
| 124 |
+
return cache[k]
|
| 125 |
+
|
| 126 |
+
from .vision import expand
|
| 127 |
+
|
| 128 |
+
ctx, preps = expand(render_state(s), dict(imgs), prepped_of, self.marks)
|
| 129 |
+
pre[i] = preps
|
| 130 |
+
plain.append((ctx, q))
|
| 131 |
+
except (ValueError, KeyError, TypeError, OSError) as e:
|
| 132 |
+
errs[i] = e
|
| 133 |
+
plain.append((s, q))
|
| 134 |
+
else:
|
| 135 |
+
plain.append((s, q))
|
| 136 |
+
encs = self.encoder.encode_many(plain)
|
| 137 |
+
for i, e in errs.items():
|
| 138 |
+
encs[i] = e
|
| 139 |
+
for i, preps in pre.items():
|
| 140 |
+
e = encs[i]
|
| 141 |
+
if isinstance(e, Exception):
|
| 142 |
+
continue
|
| 143 |
+
want = sum(p.n for p in preps)
|
| 144 |
+
got = sum(1 for t in e["ids"] if t == self.vision.image_token_id)
|
| 145 |
+
if want != got:
|
| 146 |
+
encs[i] = RuntimeError(f"image placeholder tokens: expected {want}, the tokenizer produced {got}")
|
| 147 |
+
continue
|
| 148 |
+
e["images"], e["image_tokens"] = preps, want
|
| 149 |
+
return encs
|
| 150 |
+
|
| 151 |
+
def _attach_images(self, encs: list):
|
| 152 |
+
"""Image rows: their embeddings (one tower forward for the call), image row offsets and M-RoPE positions."""
|
| 153 |
+
import numpy as np
|
| 154 |
+
|
| 155 |
+
embs = self.vision.embed([p for e in encs for p in e["images"]])
|
| 156 |
+
k = 0
|
| 157 |
+
for e in encs:
|
| 158 |
+
n = len(e["images"])
|
| 159 |
+
e["img_emb"] = torch.cat(embs[k: k + n]) if n > 1 else embs[k]
|
| 160 |
+
k += n
|
| 161 |
+
e["img_rows"] = np.nonzero(np.asarray(e["ids"]) == self.vision.image_token_id)[0]
|
| 162 |
+
e["pos3"] = self.vision.positions(e["ids"], e["images"])
|
| 163 |
+
|
| 164 |
+
def _image_probs(self, encs: list) -> list:
|
| 165 |
+
import numpy as np
|
| 166 |
+
|
| 167 |
+
out = [None] * len(encs)
|
| 168 |
+
with self.lock, torch.no_grad():
|
| 169 |
+
self._attach_images(encs)
|
| 170 |
+
if self.graphs is not None and len(encs) <= self.graphs.row_buckets[-1]:
|
| 171 |
+
for i, r in enumerate(self.graphs.probs_many([(e,) for e in encs])):
|
| 172 |
+
if r is not None:
|
| 173 |
+
out[i] = r
|
| 174 |
+
order = sorted((i for i in range(len(encs)) if out[i] is None), key=lambda i: len(encs[i]["ids"]))
|
| 175 |
+
group, tokens = [], 0
|
| 176 |
+
|
| 177 |
+
def flush():
|
| 178 |
+
from .model import Batch
|
| 179 |
+
|
| 180 |
+
b = Batch([encs[i]["ids"] for i in group], [encs[i]["slots"] for i in group], "cuda")
|
| 181 |
+
starts = np.concatenate([[0], np.cumsum([len(encs[i]["ids"]) for i in group])])
|
| 182 |
+
rows = np.concatenate([starts[j] + encs[i]["img_rows"] for j, i in enumerate(group)])
|
| 183 |
+
b.img = (torch.from_numpy(rows).to("cuda", non_blocking=True),
|
| 184 |
+
torch.cat([encs[i]["img_emb"] for i in group]))
|
| 185 |
+
b.pos3 = torch.from_numpy(np.concatenate([encs[i]["pos3"] for i in group], 1)).to("cuda",
|
| 186 |
+
non_blocking=True)
|
| 187 |
+
lg = self.model(b).cpu()
|
| 188 |
+
k = 0
|
| 189 |
+
for i in group:
|
| 190 |
+
ns = len(encs[i]["slots"])
|
| 191 |
+
rows_ = lg[k: k + ns].float().tolist()
|
| 192 |
+
k += ns
|
| 193 |
+
out[i] = [_softmax(rows_[s][: encs[i]["nopts"][s]]) for s in range(ns)]
|
| 194 |
+
|
| 195 |
+
for i in order:
|
| 196 |
+
n = len(encs[i]["ids"])
|
| 197 |
+
if group and tokens + n > self.max_batch_tokens:
|
| 198 |
+
flush()
|
| 199 |
+
group, tokens = [], 0
|
| 200 |
+
group.append(i)
|
| 201 |
+
tokens += n
|
| 202 |
+
if group:
|
| 203 |
+
flush()
|
| 204 |
+
for e in encs: # drop the device tensors (the vision cache keeps the embeddings)
|
| 205 |
+
e.pop("img_emb", None)
|
| 206 |
+
return out
|
| 207 |
+
|
| 208 |
# ---- slot probabilities -------------------------------------------------------------------------------------------
|
| 209 |
def slot_probs(self, encs: list) -> list:
|
| 210 |
+
"""Prompt rows (encoder dicts) -> per row, per slot: softmax over the slot's options (untempered). Rows with
|
| 211 |
+
images ("images" from encode_requests) run on the image path, the others exactly as text-only calls."""
|
| 212 |
+
if any(e.get("images") for e in encs):
|
| 213 |
+
img = [i for i, e in enumerate(encs) if e.get("images")]
|
| 214 |
+
txt = [i for i, e in enumerate(encs) if not e.get("images")]
|
| 215 |
+
out = [None] * len(encs)
|
| 216 |
+
for i, r in zip(img, self._image_probs([encs[i] for i in img]), strict=True):
|
| 217 |
+
out[i] = r
|
| 218 |
+
if txt:
|
| 219 |
+
for i, r in zip(txt, self.slot_probs([encs[i] for i in txt]), strict=True):
|
| 220 |
+
out[i] = r
|
| 221 |
+
return out
|
| 222 |
out = [None] * len(encs)
|
| 223 |
with self.lock, torch.no_grad():
|
| 224 |
if encs and self.graphs is not None and len(encs) <= self.graphs.row_buckets[-1]:
|
|
|
|
| 248 |
def per_question_probs(self, rows: list) -> list:
|
| 249 |
"""The per-question rows of one request: their longest common prefix computed once (exact up to bf16
|
| 250 |
round-off), else as an ordinary batch."""
|
| 251 |
+
if len(rows) > 1 and not any(e.get("images") for e in rows):
|
| 252 |
m = min(len(e["ids"]) for e in rows)
|
| 253 |
first = rows[0]["ids"]
|
| 254 |
lcp = 0
|
qev_fast/vision.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Image input for the fast engine: the same images, prompt layout and positions as qev_image.QEVImage.
|
| 2 |
+
|
| 3 |
+
- `fit_size` / `prep`: an image (PIL or path) resized with aspect kept to at most max_pixels (default 448*448, 196
|
| 4 |
+
image tokens) and at least min_pixels, each side a multiple of 32, then the checkpoint's own image processor
|
| 5 |
+
(preprocessor_config.json, do_resize=False) -> pixel patches, grid (t, h, w) and N = h * w / 4 image tokens.
|
| 6 |
+
- `expand`: "<image:NAME>" markers in the rendered state replaced by <|vision_start|> + <|image_pad|> x N +
|
| 7 |
+
<|vision_end|>; an image the state does not mention is appended at the end of the context.
|
| 8 |
+
- `Vision`: the checkpoint's vision tower + merger (the `model.visual.*` tensors, the base model's) on the GPU, run
|
| 9 |
+
once per distinct image of a call (all images of the call packed in one tower forward), with a small cache keyed by
|
| 10 |
+
the image's pixel content.
|
| 11 |
+
- `positions`: the 3-D M-RoPE positions of a prompt row (transformers' Qwen3.5 get_rope_index): text tokens
|
| 12 |
+
advance t = h = w by one; an image of llm grid (gh, gw) starting at p takes t = p, h = p + row, w = p + column and
|
| 13 |
+
the text after it continues at p + max(gh, gw).
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import hashlib
|
| 19 |
+
import json
|
| 20 |
+
import math
|
| 21 |
+
import os
|
| 22 |
+
import re
|
| 23 |
+
from collections import OrderedDict
|
| 24 |
+
|
| 25 |
+
import numpy as np
|
| 26 |
+
import torch
|
| 27 |
+
|
| 28 |
+
IMAGE_MARK = re.compile(r"<image:([^<>\s]+)>")
|
| 29 |
+
FACTOR = 32 # patch 16 x merge 2
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def fit_size(w: int, h: int, min_pixels: int, max_pixels: int, factor: int = FACTOR) -> tuple[int, int]:
|
| 33 |
+
"""(width, height) a multiple of `factor` each, aspect kept, min_pixels <= w*h <= max_pixels (qev_image.fit_size)."""
|
| 34 |
+
hh = max(factor, round(h / factor) * factor)
|
| 35 |
+
ww = max(factor, round(w / factor) * factor)
|
| 36 |
+
if hh * ww > max_pixels:
|
| 37 |
+
beta = math.sqrt(h * w / max_pixels)
|
| 38 |
+
hh, ww = max(factor, math.floor(h / beta / factor) * factor), max(factor, math.floor(w / beta / factor) * factor)
|
| 39 |
+
elif hh * ww < min_pixels:
|
| 40 |
+
beta = math.sqrt(min_pixels / (h * w))
|
| 41 |
+
hh, ww = math.ceil(h * beta / factor) * factor, math.ceil(w * beta / factor) * factor
|
| 42 |
+
return ww, hh
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _open(im):
|
| 46 |
+
from PIL import Image
|
| 47 |
+
|
| 48 |
+
if isinstance(im, (str, os.PathLike)):
|
| 49 |
+
im = Image.open(im)
|
| 50 |
+
return im.convert("RGB")
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class Prepped:
|
| 54 |
+
__slots__ = ("key", "n", "pix", "thw")
|
| 55 |
+
|
| 56 |
+
def __init__(self, pix, thw, n, key):
|
| 57 |
+
self.pix, self.thw, self.n, self.key = pix, thw, n, key
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class Vision:
|
| 61 |
+
"""The vision tower + merger of a Qwen3.5 image-text checkpoint, and the image side of the prompt."""
|
| 62 |
+
|
| 63 |
+
def __init__(self, path: str, device="cuda", dtype=torch.bfloat16, cache_size: int = 256):
|
| 64 |
+
from transformers import AutoImageProcessor, AutoModelForImageTextToText
|
| 65 |
+
|
| 66 |
+
cfg = json.load(open(os.path.join(path, "config.json")))
|
| 67 |
+
self.image_token_id = cfg["image_token_id"]
|
| 68 |
+
self.start_id, self.end_id = cfg["vision_start_token_id"], cfg["vision_end_token_id"]
|
| 69 |
+
self.merge = cfg.get("vision_config", {}).get("spatial_merge_size", 2)
|
| 70 |
+
self.ip = AutoImageProcessor.from_pretrained(path)
|
| 71 |
+
# the full image-text model as qev_image loads it (same dtype handling); only its vision tower is kept
|
| 72 |
+
vlm = AutoModelForImageTextToText.from_pretrained(path, dtype=dtype)
|
| 73 |
+
self.visual = vlm.model.visual.to(device).eval()
|
| 74 |
+
for p in self.visual.parameters():
|
| 75 |
+
p.requires_grad_(False)
|
| 76 |
+
del vlm
|
| 77 |
+
self.device, self.dtype = torch.device(device), dtype
|
| 78 |
+
self.cache: OrderedDict = OrderedDict()
|
| 79 |
+
self.cache_size = cache_size
|
| 80 |
+
self.stats = {"images": 0, "cache_hits": 0, "tower_calls": 0}
|
| 81 |
+
|
| 82 |
+
def tokens(self, tok) -> tuple[str, str, str]:
|
| 83 |
+
c = tok.convert_ids_to_tokens
|
| 84 |
+
return c(self.start_id), c(self.image_token_id), c(self.end_id)
|
| 85 |
+
|
| 86 |
+
def prep(self, im, min_pixels: int, max_pixels: int) -> Prepped:
|
| 87 |
+
"""PIL / path -> pixel patches [P, D] (CPU), grid [1, 3], image tokens, content key."""
|
| 88 |
+
im = _open(im)
|
| 89 |
+
w, h = fit_size(im.size[0], im.size[1], min_pixels, max_pixels)
|
| 90 |
+
im = im.resize((w, h), 3)
|
| 91 |
+
key = hashlib.blake2b(im.tobytes() + f"{w}x{h}".encode(), digest_size=16).hexdigest()
|
| 92 |
+
out = self.ip(images=[im], do_resize=False, return_tensors="pt")
|
| 93 |
+
thw = out["image_grid_thw"]
|
| 94 |
+
pix = out["pixel_values"]
|
| 95 |
+
return Prepped(pix, thw, int(thw.prod(-1).sum()) // (self.merge ** 2), key)
|
| 96 |
+
|
| 97 |
+
@torch.no_grad()
|
| 98 |
+
def embed(self, preps: list) -> list:
|
| 99 |
+
"""Prepped images -> their merged embeddings [N_i, H] on the device (distinct uncached images in one tower
|
| 100 |
+
forward, packed by grid)."""
|
| 101 |
+
todo, seen = [], set()
|
| 102 |
+
for p in preps:
|
| 103 |
+
if p.key not in self.cache and p.key not in seen:
|
| 104 |
+
todo.append(p)
|
| 105 |
+
seen.add(p.key)
|
| 106 |
+
self.stats["images"] += len(preps)
|
| 107 |
+
self.stats["cache_hits"] += len(preps) - len(todo)
|
| 108 |
+
if todo:
|
| 109 |
+
pix = torch.cat([p.pix for p in todo]).to(self.device, self.visual.dtype)
|
| 110 |
+
thw = torch.cat([p.thw for p in todo]).to(self.device)
|
| 111 |
+
out = self.visual(pix, grid_thw=thw, return_dict=True).pooler_output
|
| 112 |
+
self.stats["tower_calls"] += 1
|
| 113 |
+
for p, e in zip(todo, torch.split(out, [p.n for p in todo]), strict=True):
|
| 114 |
+
self.cache[p.key] = e
|
| 115 |
+
while len(self.cache) > self.cache_size:
|
| 116 |
+
self.cache.popitem(last=False)
|
| 117 |
+
res = []
|
| 118 |
+
for p in preps:
|
| 119 |
+
self.cache.move_to_end(p.key)
|
| 120 |
+
res.append(self.cache[p.key])
|
| 121 |
+
return res
|
| 122 |
+
|
| 123 |
+
def positions(self, ids: list, preps: list) -> np.ndarray:
|
| 124 |
+
"""[3, L] M-RoPE positions of one prompt row whose image runs are `preps` in order."""
|
| 125 |
+
ids = np.asarray(ids)
|
| 126 |
+
pos = np.empty((3, len(ids)), np.int64)
|
| 127 |
+
is_img = ids == self.image_token_id
|
| 128 |
+
cur, i, k, L = 0, 0, 0, len(ids)
|
| 129 |
+
while i < L:
|
| 130 |
+
if not is_img[i]:
|
| 131 |
+
j = i
|
| 132 |
+
while j < L and not is_img[j]:
|
| 133 |
+
j += 1
|
| 134 |
+
pos[:, i:j] = np.arange(cur, cur + j - i)[None]
|
| 135 |
+
cur += j - i
|
| 136 |
+
i = j
|
| 137 |
+
continue
|
| 138 |
+
p = preps[k]
|
| 139 |
+
k += 1
|
| 140 |
+
_, gh, gw = (int(x) for x in p.thw[0].tolist())
|
| 141 |
+
gh, gw = gh // self.merge, gw // self.merge
|
| 142 |
+
assert gh * gw == p.n and bool(is_img[i: i + p.n].all()), "image placeholder run does not match its image"
|
| 143 |
+
hh, ww = np.meshgrid(np.arange(gh), np.arange(gw), indexing="ij")
|
| 144 |
+
pos[0, i: i + p.n] = cur
|
| 145 |
+
pos[1, i: i + p.n] = cur + hh.reshape(-1)
|
| 146 |
+
pos[2, i: i + p.n] = cur + ww.reshape(-1)
|
| 147 |
+
cur += max(gh, gw)
|
| 148 |
+
i += p.n
|
| 149 |
+
assert k == len(preps), "more images than placeholder runs"
|
| 150 |
+
return pos
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def expand(ctx: str, images: dict, prepped_of, marks: tuple[str, str, str]) -> tuple[str, list]:
|
| 154 |
+
"""The rendered state with every image's placeholder run (qev_image.QEVImage._row) -> (context string, the
|
| 155 |
+
Prepped images in occurrence order). prepped_of(name) -> Prepped."""
|
| 156 |
+
named = []
|
| 157 |
+
for m in IMAGE_MARK.finditer(ctx):
|
| 158 |
+
if m.group(1) not in images:
|
| 159 |
+
raise ValueError(f"the state mentions <image:{m.group(1)}> but no such image was given")
|
| 160 |
+
named.append(m.group(1))
|
| 161 |
+
for k in images: # an image the state does not mention goes at the end
|
| 162 |
+
if k not in named:
|
| 163 |
+
ctx += f"\n<image:{k}>"
|
| 164 |
+
named.append(k)
|
| 165 |
+
prepped = {k: prepped_of(k) for k in dict.fromkeys(named)}
|
| 166 |
+
s, pad, e = marks
|
| 167 |
+
ctx = IMAGE_MARK.sub(lambda m: s + pad * prepped[m.group(1)].n + e, ctx)
|
| 168 |
+
return ctx, [prepped[k] for k in named]
|
qev_server.py
CHANGED
|
@@ -14,8 +14,11 @@
|
|
| 14 |
|
| 15 |
Endpoints:
|
| 16 |
- POST /v1/systemone {"state": str | object | array, "questions": {id: question}, "model": optional, "encoding":
|
| 17 |
-
optional "joint" | "per_question"
|
| 18 |
-
System One API works unchanged; "model" in
|
|
|
|
|
|
|
|
|
|
| 19 |
- GET /health readiness, backend, encoding, model files' SHA-256 (from SHA256SUMS when present).
|
| 20 |
- GET /v1/models the served model.
|
| 21 |
|
|
@@ -49,14 +52,14 @@ class Batcher:
|
|
| 49 |
self.q: queue.Queue = queue.Queue()
|
| 50 |
threading.Thread(target=self._loop, daemon=True).start()
|
| 51 |
|
| 52 |
-
def submit(self, state, questions: dict, per_question: bool, timeout: float = 300.0):
|
| 53 |
done, box = threading.Event(), {}
|
| 54 |
-
self.q.put((state, questions, per_question, done, box))
|
| 55 |
if not done.wait(timeout):
|
| 56 |
raise TimeoutError("the model did not answer in time")
|
| 57 |
if "error" in box:
|
| 58 |
raise box["error"]
|
| 59 |
-
return box["answers"], box["tokens"]
|
| 60 |
|
| 61 |
def _loop(self):
|
| 62 |
while True:
|
|
@@ -73,7 +76,7 @@ class Batcher:
|
|
| 73 |
|
| 74 |
def _run(self, jobs: list, per_question: bool):
|
| 75 |
try:
|
| 76 |
-
res = self.decide_batch([(j[0], j[1]) for j in jobs], per_question)
|
| 77 |
except Exception as e: # noqa: BLE001 - one bad request must not fail the others: answer each on its own
|
| 78 |
if len(jobs) > 1:
|
| 79 |
for j in jobs:
|
|
@@ -82,8 +85,9 @@ class Batcher:
|
|
| 82 |
jobs[0][4]["error"] = e
|
| 83 |
jobs[0][3].set()
|
| 84 |
return
|
| 85 |
-
for j,
|
| 86 |
-
j[4]["answers"], j[4]["tokens"] =
|
|
|
|
| 87 |
j[3].set()
|
| 88 |
|
| 89 |
|
|
@@ -108,6 +112,33 @@ def _validate(req) -> tuple:
|
|
| 108 |
return req["state"], qs, enc
|
| 109 |
|
| 110 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
def _file_hashes(path: str | None) -> dict:
|
| 112 |
if path and os.path.exists(os.path.join(path, "SHA256SUMS")):
|
| 113 |
out = {}
|
|
@@ -167,20 +198,26 @@ def make_handler(batcher: Batcher, info: dict, encoding: str, api_key: str | Non
|
|
| 167 |
n = int(self.headers.get("Content-Length", 0))
|
| 168 |
if n > MAX_BODY:
|
| 169 |
return self._send(413, {"detail": f"the body is larger than {MAX_BODY} bytes"})
|
| 170 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
except (ValueError, json.JSONDecodeError) as e:
|
| 172 |
return self._send(422, {"detail": str(e)})
|
| 173 |
t = time.perf_counter()
|
| 174 |
try:
|
| 175 |
-
ans, toks = batcher.submit(state, qs, (enc or encoding) == "per_question")
|
| 176 |
except (ValueError, KeyError, TypeError, AssertionError) as e:
|
| 177 |
return self._send(422, {"detail": str(e) or type(e).__name__})
|
| 178 |
except TimeoutError as e:
|
| 179 |
return self._send(503, {"detail": str(e)})
|
| 180 |
except Exception as e: # noqa: BLE001
|
| 181 |
return self._send(500, {"detail": f"{type(e).__name__}: {e}"})
|
| 182 |
-
|
| 183 |
-
|
|
|
|
|
|
|
| 184 |
"latency_ms": round((time.perf_counter() - t) * 1000, 2)})
|
| 185 |
|
| 186 |
return H
|
|
@@ -201,17 +238,27 @@ def main():
|
|
| 201 |
ap.add_argument("--fast-start", action="store_true",
|
| 202 |
help="fast backend: listen once the common CUDA graphs are captured and capture the rest in the "
|
| 203 |
"background (requests during that time can be slow); default: capture all before listening")
|
|
|
|
|
|
|
|
|
|
| 204 |
ap.add_argument("--verbose", action="store_true", help="log every request")
|
| 205 |
a = ap.parse_args()
|
| 206 |
|
| 207 |
from qev import QEV
|
| 208 |
|
| 209 |
t0 = time.perf_counter()
|
| 210 |
-
m = QEV.from_pretrained(a.model, device=a.device, backend=a.backend, precapture=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
if m.fast is not None: # CUDA graphs for every batch shape, so no live request waits for a capture
|
| 212 |
print("qev_server: capturing CUDA graphs (about 2 min the first time, faster with a warm kernel cache)",
|
| 213 |
flush=True)
|
| 214 |
-
m.fast.precapture(background=a.fast_start, per_question=a.encoding == "per_question")
|
| 215 |
m.decide("warm-up", {"q": {"type": "noul", "instructions": "Is this a warm-up?"}})
|
| 216 |
local = a.model if os.path.isdir(a.model) else None
|
| 217 |
if local is None:
|
|
@@ -222,9 +269,13 @@ def main():
|
|
| 222 |
except Exception: # noqa: BLE001
|
| 223 |
local = None
|
| 224 |
info = {"model": "qev-0.8b", "source": a.model, "backend": m.backend, "device": str(m.device),
|
| 225 |
-
"encoding": a.encoding, "load_s": round(time.perf_counter() - t0, 1), "sha256": _file_hashes(local),
|
| 226 |
"verbose": a.verbose}
|
| 227 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
key = os.environ.get("QEV_API_KEY") or None
|
| 229 |
httpd = Server((a.host, a.port), make_handler(batcher, info, a.encoding, key))
|
| 230 |
print(f"qev_server: {info['backend']} backend on {info['device']}, ready in {info['load_s']} s; "
|
|
|
|
| 14 |
|
| 15 |
Endpoints:
|
| 16 |
- POST /v1/systemone {"state": str | object | array, "questions": {id: question}, "model": optional, "encoding":
|
| 17 |
+
optional "joint" | "per_question", "images": optional {name: base64 PNG / JPEG}} ->
|
| 18 |
+
{"model", "answers", "usage"}. Code written for the System One API works unchanged; "model" in
|
| 19 |
+
the request is accepted and ignored. Images (fast CUDA backend; pillow + torchvision): the
|
| 20 |
+
state places one with "<image:name>", an image it does not mention goes at the end of the
|
| 21 |
+
context; usage then also reports "image_tokens".
|
| 22 |
- GET /health readiness, backend, encoding, model files' SHA-256 (from SHA256SUMS when present).
|
| 23 |
- GET /v1/models the served model.
|
| 24 |
|
|
|
|
| 52 |
self.q: queue.Queue = queue.Queue()
|
| 53 |
threading.Thread(target=self._loop, daemon=True).start()
|
| 54 |
|
| 55 |
+
def submit(self, state, questions: dict, per_question: bool, timeout: float = 300.0, images: dict | None = None):
|
| 56 |
done, box = threading.Event(), {}
|
| 57 |
+
self.q.put((state, questions, per_question, done, box, images))
|
| 58 |
if not done.wait(timeout):
|
| 59 |
raise TimeoutError("the model did not answer in time")
|
| 60 |
if "error" in box:
|
| 61 |
raise box["error"]
|
| 62 |
+
return box["answers"], box["tokens"], box.get("image_tokens", 0)
|
| 63 |
|
| 64 |
def _loop(self):
|
| 65 |
while True:
|
|
|
|
| 76 |
|
| 77 |
def _run(self, jobs: list, per_question: bool):
|
| 78 |
try:
|
| 79 |
+
res = self.decide_batch([(j[0], j[1], j[5]) if j[5] else (j[0], j[1]) for j in jobs], per_question)
|
| 80 |
except Exception as e: # noqa: BLE001 - one bad request must not fail the others: answer each on its own
|
| 81 |
if len(jobs) > 1:
|
| 82 |
for j in jobs:
|
|
|
|
| 85 |
jobs[0][4]["error"] = e
|
| 86 |
jobs[0][3].set()
|
| 87 |
return
|
| 88 |
+
for j, r in zip(jobs, res, strict=True):
|
| 89 |
+
j[4]["answers"], j[4]["tokens"] = r[0], r[1]
|
| 90 |
+
j[4]["image_tokens"] = r[2] if len(r) > 2 else 0
|
| 91 |
j[3].set()
|
| 92 |
|
| 93 |
|
|
|
|
| 112 |
return req["state"], qs, enc
|
| 113 |
|
| 114 |
|
| 115 |
+
def decode_images(raw) -> dict | None:
|
| 116 |
+
"""The "images" field: {name: base64 string (a data: URL prefix is accepted)} -> {name: PIL image}."""
|
| 117 |
+
import base64
|
| 118 |
+
import binascii
|
| 119 |
+
import io
|
| 120 |
+
|
| 121 |
+
if raw is None or raw == {}:
|
| 122 |
+
return None
|
| 123 |
+
if not isinstance(raw, dict):
|
| 124 |
+
raise ValueError('"images" must be an object {name: base64 string}')
|
| 125 |
+
try:
|
| 126 |
+
from PIL import Image
|
| 127 |
+
except ImportError:
|
| 128 |
+
raise ValueError("images need pillow (pip install pillow torchvision)") from None
|
| 129 |
+
out = {}
|
| 130 |
+
for k, v in raw.items():
|
| 131 |
+
if not isinstance(v, str):
|
| 132 |
+
raise ValueError(f'image "{k}": a base64 string')
|
| 133 |
+
if v.startswith("data:"):
|
| 134 |
+
v = v.split(",", 1)[-1]
|
| 135 |
+
try:
|
| 136 |
+
out[k] = Image.open(io.BytesIO(base64.b64decode(v, validate=True))).convert("RGB")
|
| 137 |
+
except (binascii.Error, OSError) as e:
|
| 138 |
+
raise ValueError(f'image "{k}": not a decodable base64 image ({e})') from None
|
| 139 |
+
return out
|
| 140 |
+
|
| 141 |
+
|
| 142 |
def _file_hashes(path: str | None) -> dict:
|
| 143 |
if path and os.path.exists(os.path.join(path, "SHA256SUMS")):
|
| 144 |
out = {}
|
|
|
|
| 198 |
n = int(self.headers.get("Content-Length", 0))
|
| 199 |
if n > MAX_BODY:
|
| 200 |
return self._send(413, {"detail": f"the body is larger than {MAX_BODY} bytes"})
|
| 201 |
+
body = json.loads(self.rfile.read(n) or b"{}")
|
| 202 |
+
state, qs, enc = _validate(body)
|
| 203 |
+
imgs = decode_images(body.get("images"))
|
| 204 |
+
if imgs and not info.get("images"):
|
| 205 |
+
raise ValueError("this server does not read images (the fast CUDA backend is needed)")
|
| 206 |
except (ValueError, json.JSONDecodeError) as e:
|
| 207 |
return self._send(422, {"detail": str(e)})
|
| 208 |
t = time.perf_counter()
|
| 209 |
try:
|
| 210 |
+
ans, toks, itoks = batcher.submit(state, qs, (enc or encoding) == "per_question", images=imgs)
|
| 211 |
except (ValueError, KeyError, TypeError, AssertionError) as e:
|
| 212 |
return self._send(422, {"detail": str(e) or type(e).__name__})
|
| 213 |
except TimeoutError as e:
|
| 214 |
return self._send(503, {"detail": str(e)})
|
| 215 |
except Exception as e: # noqa: BLE001
|
| 216 |
return self._send(500, {"detail": f"{type(e).__name__}: {e}"})
|
| 217 |
+
usage = {"input_tokens": toks, "output_tokens": 0}
|
| 218 |
+
if imgs:
|
| 219 |
+
usage["image_tokens"] = itoks
|
| 220 |
+
return self._send(200, {"model": info["model"], "answers": ans, "usage": usage,
|
| 221 |
"latency_ms": round((time.perf_counter() - t) * 1000, 2)})
|
| 222 |
|
| 223 |
return H
|
|
|
|
| 238 |
ap.add_argument("--fast-start", action="store_true",
|
| 239 |
help="fast backend: listen once the common CUDA graphs are captured and capture the rest in the "
|
| 240 |
"background (requests during that time can be slow); default: capture all before listening")
|
| 241 |
+
ap.add_argument("--no-images", action="store_true",
|
| 242 |
+
help="do not load the vision tower (requests with \"images\" then get HTTP 422)")
|
| 243 |
+
ap.add_argument("--max-pixels", type=int, default=448 * 448, help="images are resized to at most this many pixels")
|
| 244 |
ap.add_argument("--verbose", action="store_true", help="log every request")
|
| 245 |
a = ap.parse_args()
|
| 246 |
|
| 247 |
from qev import QEV
|
| 248 |
|
| 249 |
t0 = time.perf_counter()
|
| 250 |
+
m = QEV.from_pretrained(a.model, device=a.device, backend=a.backend, precapture=False, max_pixels=a.max_pixels)
|
| 251 |
+
images = False
|
| 252 |
+
if m.fast is not None and not a.no_images:
|
| 253 |
+
try:
|
| 254 |
+
m.fast.enable_images(a.max_pixels)
|
| 255 |
+
images = True
|
| 256 |
+
except Exception as e: # noqa: BLE001 - no pillow / torchvision / processor config: a text-only server
|
| 257 |
+
print(f"qev_server: images off ({type(e).__name__}: {e})", flush=True)
|
| 258 |
if m.fast is not None: # CUDA graphs for every batch shape, so no live request waits for a capture
|
| 259 |
print("qev_server: capturing CUDA graphs (about 2 min the first time, faster with a warm kernel cache)",
|
| 260 |
flush=True)
|
| 261 |
+
m.fast.precapture(background=a.fast_start, per_question=a.encoding == "per_question", images=images)
|
| 262 |
m.decide("warm-up", {"q": {"type": "noul", "instructions": "Is this a warm-up?"}})
|
| 263 |
local = a.model if os.path.isdir(a.model) else None
|
| 264 |
if local is None:
|
|
|
|
| 269 |
except Exception: # noqa: BLE001
|
| 270 |
local = None
|
| 271 |
info = {"model": "qev-0.8b", "source": a.model, "backend": m.backend, "device": str(m.device),
|
| 272 |
+
"encoding": a.encoding, "images": images, "load_s": round(time.perf_counter() - t0, 1), "sha256": _file_hashes(local),
|
| 273 |
"verbose": a.verbose}
|
| 274 |
+
def run(reqs, pq):
|
| 275 |
+
res = m.decide_batch(reqs, pq, usage=True)
|
| 276 |
+
return [(a_, t_, k_) for (a_, t_), k_ in zip(res, m.last_image_tokens, strict=True)]
|
| 277 |
+
|
| 278 |
+
batcher = Batcher(run, max_batch=a.max_batch)
|
| 279 |
key = os.environ.get("QEV_API_KEY") or None
|
| 280 |
httpd = Server((a.host, a.port), make_handler(batcher, info, a.encoding, key))
|
| 281 |
print(f"qev_server: {info['backend']} backend on {info['device']}, ready in {info['load_s']} s; "
|