Text Classification
Transformers
Safetensors
English
nli
cross-encoder
qwen3.5
reranker
image-text-to-text
Instructions to use AlexWortega/openjev with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use AlexWortega/openjev with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-classification", model="AlexWortega/openjev")# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("AlexWortega/openjev", device_map="auto") - Notebooks
- Google Colab
- Kaggle
SGLang serving: external model package, launcher, client, benchmark; card section
Browse files- README.md +38 -1
- code/bench_sglang.py +95 -0
- code/serve_sglang.sh +17 -0
- code/sglang_client.py +92 -0
- code/sglang_openjev/__init__.py +2 -0
- code/sglang_openjev/qwen3_5_seqcls.py +48 -0
README.md
CHANGED
|
@@ -64,7 +64,7 @@ a small MLP head on the last-token latent (`mlp_heads_35b/`, one head per task,
|
|
| 64 |
* `modeling_qwen35_moe_seqcls.py` — `Qwen3_5MoeForSequenceClassification` for the 35B-A3B backbone.
|
| 65 |
* `mlp_heads_35b/<task>/` — `head.pt` + `norm.npz` + `meta.json`, the 35B latent + MLP heads behind the second radar.
|
| 66 |
* `code/` — everything used here: the trainer and data mixture builder, the evaluation harness, Flappy Bird, Doom
|
| 67 |
-
(text and pixels), the Minecraft scaffold and bot, the radar.
|
| 68 |
* `videos/` — Flappy Bird, Doom and Minecraft replays; `results/` — raw JSON for every run and the full report.
|
| 69 |
|
| 70 |
## Use it
|
|
@@ -104,4 +104,41 @@ text = model.config.nli_template.format(premise="...", hypothesis="...")
|
|
| 104 |
Images go inside the premise as `<|vision_start|><|image_pad|>…<|vision_end|>` with `pixel_values` / `image_grid_thw`
|
| 105 |
from the Qwen3.5 image processor; see `code/doom_vision.py` and `code/eval_image_nli.py`.
|
| 106 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
Reference point: [dleemiller's NLI cross-encoders](https://huggingface.co/blog/dleemiller/nli-xenc-ways-to-use). Licence MIT.
|
|
|
|
| 64 |
* `modeling_qwen35_moe_seqcls.py` — `Qwen3_5MoeForSequenceClassification` for the 35B-A3B backbone.
|
| 65 |
* `mlp_heads_35b/<task>/` — `head.pt` + `norm.npz` + `meta.json`, the 35B latent + MLP heads behind the second radar.
|
| 66 |
* `code/` — everything used here: the trainer and data mixture builder, the evaluation harness, Flappy Bird, Doom
|
| 67 |
+
(text and pixels), the Minecraft scaffold and bot, the radar, the SGLang package / launcher / client / benchmark.
|
| 68 |
* `videos/` — Flappy Bird, Doom and Minecraft replays; `results/` — raw JSON for every run and the full report.
|
| 69 |
|
| 70 |
## Use it
|
|
|
|
| 104 |
Images go inside the premise as `<|vision_start|><|image_pad|>…<|vision_end|>` with `pixel_values` / `image_grid_thw`
|
| 105 |
from the Qwen3.5 image processor; see `code/doom_vision.py` and `code/eval_image_nli.py`.
|
| 106 |
|
| 107 |
+
## Serve it with SGLang
|
| 108 |
+
|
| 109 |
+
SGLang has no sequence-classification class for Qwen3.5, so `code/sglang_openjev/` is an external model package
|
| 110 |
+
that adds the `score` head to SGLang's `Qwen3_5ForConditionalGeneration` (hybrid cache, mrope and the vision tower
|
| 111 |
+
stay as they are). Text and images both work; `/classify` returns the three raw logits.
|
| 112 |
+
|
| 113 |
+
```bash
|
| 114 |
+
hf download AlexWortega/openjev --include "qwen3.5-0.8b-nli-v2s-long/*" "code/*" --local-dir openjev
|
| 115 |
+
cd openjev/code && bash serve_sglang.sh ../qwen3.5-0.8b-nli-v2s-long 30000 # tested with sglang 0.5.19
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
```python
|
| 119 |
+
from sglang_client import OpenJevSGLang # code/sglang_client.py
|
| 120 |
+
jev = OpenJevSGLang("http://127.0.0.1:30000")
|
| 121 |
+
jev.predict([("A man is playing a guitar.", "Someone is making music.")]) # [[con, ent, neu]]
|
| 122 |
+
jev.predict([("A photograph of a scene:", "There is a dog.")], images=["dog.jpg"])
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
Without the script: `SGLANG_EXTERNAL_MODEL_PACKAGE=sglang_openjev` with `code/` on `PYTHONPATH`, then
|
| 126 |
+
`python -m sglang.launch_server --model-path <dir> --is-embedding --json-model-override-args
|
| 127 |
+
'{"architectures": ["Qwen3_5ForConditionalGeneration"]}'`.
|
| 128 |
+
|
| 129 |
+
Same predictions as transformers, 1.5-3x the throughput. `qwen3.5-0.8b-nli-v2s-long` on one RTX A6000 that was
|
| 130 |
+
shared with another job (so absolute speed is a lower bound), transformers at batch 32 (`code/bench_sglang.py`):
|
| 131 |
+
|
| 132 |
+
| task | pairs | tokens / pair | acc SGLang | acc transformers | SGLang pairs/s | transformers pairs/s | speed-up |
|
| 133 |
+
|---|---|---|---|---|---|---|---|
|
| 134 |
+
| MNLI m+mm | 19647 | 44 | 86.63 | 86.64 | 321 | 152 | 2.1x |
|
| 135 |
+
| ANLI r1 | 1000 | 105 | 65.3 | 64.9 | 189 | 119 | 1.6x |
|
| 136 |
+
| ANLI r2 | 1000 | 103 | 50.6 | 50.8 | 181 | 110 | 1.6x |
|
| 137 |
+
| ANLI r3 | 1200 | 92 | 48.6 | 48.6 | 218 | 95 | 2.3x |
|
| 138 |
+
| WANLI | 5000 | 40 | 73.7 | 73.7 | 400 | 196 | 2.0x |
|
| 139 |
+
| SciTail | 2126 | 43 | 93.3 | 93.4 | 319 | 217 | 1.5x |
|
| 140 |
+
| ConTRoL (long) | 805 | 612 | 51.9 | 51.4 | 45 | 15 | 3.1x |
|
| 141 |
+
| ARC-Challenge (rerank) | 9374 | 49 | 49.2 | 49.2 | 334 | 197 | 1.7x |
|
| 142 |
+
| HellaSwag (rerank, 2k questions) | 16000 | 114 | 37.6 | 37.5 | 176 | 109 | 1.6x |
|
| 143 |
+
|
| 144 |
Reference point: [dleemiller's NLI cross-encoders](https://huggingface.co/blog/dleemiller/nli-xenc-ways-to-use). Licence MIT.
|
code/bench_sglang.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Accuracy + throughput of an openjev checkpoint: transformers (eval.py's NLIScorer) vs the SGLang server.
|
| 2 |
+
|
| 3 |
+
./serve_sglang.sh ckpt/qwen3.5-0.8b-nli-v2s-long &
|
| 4 |
+
python bench_sglang.py --ckpt ckpt/qwen3.5-0.8b-nli-v2s-long --out results/sglang/bench.json
|
| 5 |
+
|
| 6 |
+
Both backends go through the same eval.py task code; only `predict` is timed (no dataset loading)."""
|
| 7 |
+
import argparse
|
| 8 |
+
import json
|
| 9 |
+
import os
|
| 10 |
+
import time
|
| 11 |
+
from types import SimpleNamespace
|
| 12 |
+
|
| 13 |
+
import numpy as np
|
| 14 |
+
import torch
|
| 15 |
+
|
| 16 |
+
import eval as E
|
| 17 |
+
from sglang_client import OpenJevSGLang
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class Timed:
|
| 21 |
+
def __init__(self, predict, tok, max_len):
|
| 22 |
+
self._predict, self.tok, self.max_len = predict, tok, max_len
|
| 23 |
+
self.reset()
|
| 24 |
+
|
| 25 |
+
def reset(self):
|
| 26 |
+
self.sec, self.pairs, self.tokens = 0.0, 0, 0
|
| 27 |
+
|
| 28 |
+
def predict(self, pairs):
|
| 29 |
+
t = time.perf_counter()
|
| 30 |
+
out = self._predict(pairs)
|
| 31 |
+
if torch.cuda.is_available():
|
| 32 |
+
torch.cuda.synchronize()
|
| 33 |
+
self.sec += time.perf_counter() - t
|
| 34 |
+
self.pairs += len(pairs)
|
| 35 |
+
texts = [E_TEMPLATE.format(premise=p.strip(), hypothesis=h.strip()) for p, h in pairs]
|
| 36 |
+
self.tokens += sum(min(len(x), self.max_len) for x in self.tok(texts)["input_ids"])
|
| 37 |
+
return out
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def run(scorer, tasks, mc_items, mnli_n):
|
| 41 |
+
res = {}
|
| 42 |
+
for t in tasks:
|
| 43 |
+
scorer.reset()
|
| 44 |
+
if t == "mnli":
|
| 45 |
+
r = E.eval_mnli(scorer, mnli_n)
|
| 46 |
+
acc = float(np.mean([v["acc"] for v in r.values()]))
|
| 47 |
+
elif t in E.NLI_SETS:
|
| 48 |
+
acc = E.eval_nli_set(scorer, t)["acc"]
|
| 49 |
+
else:
|
| 50 |
+
acc = E.eval_mc(scorer, mc_items[t])["rerank_acc"]
|
| 51 |
+
res[t] = {"acc": acc, "pairs": scorer.pairs, "tokens": scorer.tokens, "sec": round(scorer.sec, 2),
|
| 52 |
+
"pairs_per_s": round(scorer.pairs / scorer.sec, 1), "tok_per_s": round(scorer.tokens / scorer.sec)}
|
| 53 |
+
print(t, res[t], flush=True)
|
| 54 |
+
return res
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
if __name__ == "__main__":
|
| 58 |
+
ap = argparse.ArgumentParser()
|
| 59 |
+
ap.add_argument("--ckpt", required=True)
|
| 60 |
+
ap.add_argument("--url", default="http://127.0.0.1:30000")
|
| 61 |
+
ap.add_argument("--out", required=True)
|
| 62 |
+
ap.add_argument("--tasks", nargs="+", default=["mnli", "anli_r1", "anli_r2", "anli_r3", "wanli", "scitail", "control",
|
| 63 |
+
"arc_challenge", "hellaswag"])
|
| 64 |
+
ap.add_argument("--backends", nargs="+", default=["sglang", "hf"])
|
| 65 |
+
ap.add_argument("--mnli-n", type=int, default=None)
|
| 66 |
+
ap.add_argument("--mc-n", type=int, default=2000, help="hellaswag / mmlu subsample")
|
| 67 |
+
ap.add_argument("--bs", type=int, default=32)
|
| 68 |
+
ap.add_argument("--max-len", type=int, default=4096)
|
| 69 |
+
ap.add_argument("--workers", type=int, default=16)
|
| 70 |
+
a = ap.parse_args()
|
| 71 |
+
from transformers import AutoConfig, AutoTokenizer
|
| 72 |
+
|
| 73 |
+
tok = AutoTokenizer.from_pretrained(a.ckpt)
|
| 74 |
+
E_TEMPLATE = AutoConfig.from_pretrained(a.ckpt).nli_template
|
| 75 |
+
margs = SimpleNamespace(mc_n=a.mc_n, chess_n=500, fewshot=5)
|
| 76 |
+
mc_items = {t: E.MC_TASKS[t](margs) for t in a.tasks if t in E.MC_TASKS}
|
| 77 |
+
results = {}
|
| 78 |
+
for b in a.backends:
|
| 79 |
+
print(f"\n===== {b}")
|
| 80 |
+
if b == "hf":
|
| 81 |
+
model = E.NLIScorer(a.ckpt, bs=a.bs, max_len=a.max_len)
|
| 82 |
+
predict = model.predict
|
| 83 |
+
else:
|
| 84 |
+
predict = OpenJevSGLang(a.url, template=E_TEMPLATE, bs=a.bs, workers=a.workers).predict
|
| 85 |
+
predict([("warm", "up")] * a.bs)
|
| 86 |
+
results[b] = run(Timed(predict, tok, a.max_len), a.tasks, mc_items, a.mnli_n)
|
| 87 |
+
os.makedirs(os.path.dirname(a.out) or ".", exist_ok=True)
|
| 88 |
+
json.dump(results, open(a.out, "w"), indent=2)
|
| 89 |
+
print("\n| task | pairs | avg tok | " + " | ".join(f"{b} acc | {b} pairs/s" for b in a.backends) + " | speedup |")
|
| 90 |
+
print("|---|---|---|" + "---|---|" * len(a.backends) + "---|")
|
| 91 |
+
for t in a.tasks:
|
| 92 |
+
r0 = results[a.backends[0]][t]
|
| 93 |
+
row = " | ".join(f"{results[b][t]['acc']:.4f} | {results[b][t]['pairs_per_s']}" for b in a.backends)
|
| 94 |
+
sp = r0["pairs_per_s"] / results[a.backends[-1]][t]["pairs_per_s"] if len(a.backends) > 1 else 1.0
|
| 95 |
+
print(f"| {t} | {r0['pairs']} | {r0['tokens'] // r0['pairs']} | {row} | {sp:.1f}x |")
|
code/serve_sglang.sh
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# Serve an openjev checkpoint with SGLang. Usage: ./serve_sglang.sh [ckpt dir or HF snapshot dir] [port] [extra sglang args]
|
| 3 |
+
# Needs a local dir with config.json at its root (for the Hub repo: hf download AlexWortega/openjev --include
|
| 4 |
+
# "qwen3.5-0.8b-nli-v2s-long/*" --local-dir openjev_hf). Query with sglang_client.py.
|
| 5 |
+
# ATTN_BACKEND defaults to triton: flashinfer JIT-compiles with the system nvcc and fails on nvcc < 12.4.
|
| 6 |
+
# MEM_FRACTION is a share of the WHOLE card, other processes included: on a shared GPU set it close to
|
| 7 |
+
# 1 - (wanted headroom / total), e.g. 0.85 on a 48GB card that already has 32GB taken.
|
| 8 |
+
HERE=$(cd "$(dirname "$0")" && pwd)
|
| 9 |
+
MODEL=${1:-ckpt/qwen3.5-0.8b-nli-v2s-long}
|
| 10 |
+
PORT=${2:-30000}
|
| 11 |
+
PY=${PY:-$HOME/venvs/sglang/bin/python}
|
| 12 |
+
export PATH=$(dirname "$PY"):$PATH # ninja for sglang's JIT kernels
|
| 13 |
+
export PYTHONPATH=$HERE:$PYTHONPATH
|
| 14 |
+
export SGLANG_EXTERNAL_MODEL_PACKAGE=sglang_openjev
|
| 15 |
+
exec $PY -m sglang.launch_server --model-path "$MODEL" --port "$PORT" --host 0.0.0.0 \
|
| 16 |
+
--is-embedding --json-model-override-args '{"architectures": ["Qwen3_5ForConditionalGeneration"]}' \
|
| 17 |
+
--attention-backend ${ATTN_BACKEND:-triton} --served-model-name openjev ${MEM_FRACTION:+--mem-fraction-static $MEM_FRACTION} "${@:3}"
|
code/sglang_client.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Client for an openjev checkpoint served by SGLang (serve_sglang.sh). Same interface as
|
| 2 |
+
`OpenJevCrossEncoder.predict/rerank`, plus `--check` to compare the server against transformers.
|
| 3 |
+
|
| 4 |
+
python sglang_client.py --premise "A man is playing a guitar." --hypothesis "Someone is making music."
|
| 5 |
+
python sglang_client.py --check ckpt/qwen3.5-0.8b-nli-v2s-long # max |p_sglang - p_hf| on a few pairs
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import argparse
|
| 10 |
+
import base64
|
| 11 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 12 |
+
|
| 13 |
+
import numpy as np
|
| 14 |
+
import requests
|
| 15 |
+
|
| 16 |
+
CON, ENT, NEU = 0, 1, 2
|
| 17 |
+
LABELS = ["contradiction", "entailment", "neutral"]
|
| 18 |
+
DEFAULT_TEMPLATE = "Premise: {premise}\nHypothesis: {hypothesis}"
|
| 19 |
+
IMG_MARK = "<<IMG>>"
|
| 20 |
+
IMG_BLOCK = "<|vision_start|><|image_pad|><|vision_end|>" # SGLang expands the single pad to the patch count
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class OpenJevSGLang:
|
| 24 |
+
def __init__(self, url: str = "http://127.0.0.1:30000", template: str = DEFAULT_TEMPLATE, bs: int = 32,
|
| 25 |
+
workers: int = 16):
|
| 26 |
+
# `workers` requests of `bs` pairs are in flight at once; the server batches across them
|
| 27 |
+
self.url, self.template, self.bs, self.workers = url.rstrip("/"), template, bs, workers
|
| 28 |
+
|
| 29 |
+
def _post(self, payload):
|
| 30 |
+
r = requests.post(f"{self.url}/classify", json=payload, timeout=600)
|
| 31 |
+
r.raise_for_status()
|
| 32 |
+
out = r.json()
|
| 33 |
+
return [o["embedding"] for o in (out if isinstance(out, list) else [out])]
|
| 34 |
+
|
| 35 |
+
def logits(self, pairs, images=None) -> np.ndarray:
|
| 36 |
+
"""Raw [contradiction, entailment, neutral] logits. `images[i]` is a path / URL / bytes or None and is
|
| 37 |
+
spliced into premise i at the `<<IMG>>` marker (appended if the marker is absent)."""
|
| 38 |
+
payloads = []
|
| 39 |
+
for i in range(0, len(pairs), self.bs):
|
| 40 |
+
texts, imgs = [], []
|
| 41 |
+
for j, (p, h) in enumerate(pairs[i:i + self.bs]):
|
| 42 |
+
img = images[i + j] if images else None
|
| 43 |
+
if img is not None:
|
| 44 |
+
p = p.replace(IMG_MARK, IMG_BLOCK) if IMG_MARK in p else f"{p.rstrip()} {IMG_BLOCK}"
|
| 45 |
+
img = base64.b64encode(img).decode() if isinstance(img, bytes) else img
|
| 46 |
+
texts.append(self.template.format(premise=p.strip(), hypothesis=h.strip()))
|
| 47 |
+
imgs.append(img)
|
| 48 |
+
payload = {"text": texts}
|
| 49 |
+
if any(x is not None for x in imgs):
|
| 50 |
+
payload["image_data"] = imgs
|
| 51 |
+
payloads.append(payload)
|
| 52 |
+
with ThreadPoolExecutor(self.workers) as ex:
|
| 53 |
+
out = [z for part in ex.map(self._post, payloads) for z in part]
|
| 54 |
+
return np.asarray(out, np.float32)
|
| 55 |
+
|
| 56 |
+
def predict(self, pairs, images=None) -> np.ndarray:
|
| 57 |
+
"""Softmax probabilities [contradiction, entailment, neutral] per pair."""
|
| 58 |
+
z = self.logits(pairs, images)
|
| 59 |
+
z = np.exp(z - z.max(-1, keepdims=True))
|
| 60 |
+
return z / z.sum(-1, keepdims=True)
|
| 61 |
+
|
| 62 |
+
def rerank(self, question: str, options, hyp_fmt: str = "The correct answer is: {}") -> int:
|
| 63 |
+
return int(self.predict([(question, hyp_fmt.format(o)) for o in options])[:, ENT].argmax())
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
CHECK_PAIRS = [
|
| 67 |
+
("A man is playing a guitar.", "Someone is making music."),
|
| 68 |
+
("A man is playing a guitar.", "The man is asleep."),
|
| 69 |
+
("A man is playing a guitar.", "The man is a professional musician."),
|
| 70 |
+
("The Eiffel Tower is in Paris. " * 40, "The Eiffel Tower is in Berlin."),
|
| 71 |
+
("What is the capital of France?", "The correct answer is: Paris"),
|
| 72 |
+
]
|
| 73 |
+
|
| 74 |
+
if __name__ == "__main__":
|
| 75 |
+
ap = argparse.ArgumentParser()
|
| 76 |
+
ap.add_argument("--url", default="http://127.0.0.1:30000")
|
| 77 |
+
ap.add_argument("--premise")
|
| 78 |
+
ap.add_argument("--hypothesis")
|
| 79 |
+
ap.add_argument("--image")
|
| 80 |
+
ap.add_argument("--check", metavar="CKPT", help="compare against transformers on this checkpoint")
|
| 81 |
+
a = ap.parse_args()
|
| 82 |
+
client = OpenJevSGLang(a.url)
|
| 83 |
+
if a.check:
|
| 84 |
+
from modeling_openjev import OpenJevCrossEncoder
|
| 85 |
+
|
| 86 |
+
ours, ref = client.predict(CHECK_PAIRS), OpenJevCrossEncoder(a.check).predict(CHECK_PAIRS)
|
| 87 |
+
for (p, h), x, y in zip(CHECK_PAIRS, ours, ref):
|
| 88 |
+
print(f"{h[:50]:52s} sglang {np.round(x, 4)} hf {np.round(y, 4)}")
|
| 89 |
+
print("max abs diff:", float(np.abs(ours - ref).max()), "| argmax agree:", bool((ours.argmax(1) == ref.argmax(1)).all()))
|
| 90 |
+
else:
|
| 91 |
+
p = client.predict([(a.premise, a.hypothesis)], [a.image] if a.image else None)[0]
|
| 92 |
+
print({l: round(float(v), 4) for l, v in zip(LABELS, p)})
|
code/sglang_openjev/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""SGLang external model package for openjev. Enable with SGLANG_EXTERNAL_MODEL_PACKAGE=sglang_openjev
|
| 2 |
+
(this directory's parent must be on PYTHONPATH); see serve_sglang.sh."""
|
code/sglang_openjev/qwen3_5_seqcls.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Qwen3_5ForSequenceClassification for SGLang (which ships no seq-cls head for Qwen3.5).
|
| 2 |
+
|
| 3 |
+
SGLang keys the hybrid GDN cache, the mrope setup and the Qwen-VL multimodal processor on the architecture
|
| 4 |
+
*name* `Qwen3_5ForConditionalGeneration`, so instead of registering a new name this module overrides that
|
| 5 |
+
entry (the registry loads external packages with overwrite=True) and the server is launched with
|
| 6 |
+
`--json-model-override-args '{"architectures": ["Qwen3_5ForConditionalGeneration"]}' --is-embedding`.
|
| 7 |
+
Generation is untouched; with get_embedding the last-token hidden state goes through the linear `score`
|
| 8 |
+
head, same pooling as transformers (last non-pad token), and the raw logits come back as the "embedding".
|
| 9 |
+
"""
|
| 10 |
+
from typing import Optional
|
| 11 |
+
|
| 12 |
+
import torch
|
| 13 |
+
from torch import nn
|
| 14 |
+
|
| 15 |
+
from sglang.srt.layers.pooler import Pooler, PoolingType, score_and_pool
|
| 16 |
+
from sglang.srt.layers.quantization.base_config import QuantizationConfig
|
| 17 |
+
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
|
| 18 |
+
from sglang.srt.models import qwen3_5 as _qwen3_5
|
| 19 |
+
from sglang.srt.models.qwen3_vl import general_mm_embed_routine
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class Qwen3_5ForConditionalGeneration(_qwen3_5.Qwen3_5ForConditionalGeneration):
|
| 23 |
+
def __init__(self, config, quant_config: Optional[QuantizationConfig] = None, prefix: str = ""):
|
| 24 |
+
super().__init__(config, quant_config, prefix)
|
| 25 |
+
# self.config is the text config from here on; num_labels lives on the top-level one
|
| 26 |
+
self.score = nn.Linear(self.config.hidden_size, config.num_labels, bias=False)
|
| 27 |
+
self.pooler = Pooler(pooling_type=PoolingType.LAST, normalize=False)
|
| 28 |
+
|
| 29 |
+
@torch.no_grad()
|
| 30 |
+
def forward(self, input_ids: torch.Tensor, positions: torch.Tensor, forward_batch: ForwardBatch,
|
| 31 |
+
get_embedding: bool = False, pp_proxy_tensors=None):
|
| 32 |
+
if not get_embedding:
|
| 33 |
+
return super().forward(input_ids, positions, forward_batch, False, pp_proxy_tensors)
|
| 34 |
+
if self.is_mrope_enabled:
|
| 35 |
+
positions = forward_batch.mrope_positions
|
| 36 |
+
hidden_states = general_mm_embed_routine(
|
| 37 |
+
input_ids=input_ids,
|
| 38 |
+
forward_batch=forward_batch,
|
| 39 |
+
language_model=self.model,
|
| 40 |
+
multimodal_model=self,
|
| 41 |
+
positions=positions,
|
| 42 |
+
use_deepstack=self.use_deepstack,
|
| 43 |
+
pp_proxy_tensors=pp_proxy_tensors,
|
| 44 |
+
)
|
| 45 |
+
return score_and_pool(self.score, self.pooler, hidden_states, forward_batch, input_ids)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
EntryClass = [Qwen3_5ForConditionalGeneration]
|