Skip to content

Commit 1fa1ab8

Browse files
fix(vllm-mlx-niah): use /v1/completions + resolved model id (chat 404 on raw MLX verifier)
First Mac run: vllm-mlx 0.3.0 installed + loaded gemma-4-26B-A4B-it-mlx-4bit and served (continuous batching), but every /v1/chat/completions 404'd (the MLX verifier is a raw checkpoint with no chat template; /version also 404). Harness now resolves the served model id from /v1/models and generates via /v1/completions (raw prompt), falling back to chat — so recall/throughput measure the real parallel path. Co-authored-by: FluffyAIcode <FluffyAIcode@users.noreply.github.com>
1 parent e59de20 commit 1fa1ab8

1 file changed

Lines changed: 81 additions & 35 deletions

File tree

scripts/research/vllm_mlx_niah_bench.py

Lines changed: 81 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -81,41 +81,78 @@ def _build_niah_items(sessions: int, haystack_lines: int) -> List[Dict[str, str]
8181
return items
8282

8383

84-
def _post_chat(
85-
base_url: str, prompt: str, max_new_tokens: int, timeout: float,
86-
) -> Tuple[bool, str, int, float, str]:
87-
"""POST /v1/chat/completions. Returns (ok, text, completion_tokens, latency, err)."""
88-
body = json.dumps({
89-
"model": "default",
90-
"messages": [{"role": "user", "content": prompt}],
91-
"max_tokens": int(max_new_tokens),
92-
"temperature": 0.0,
93-
}).encode("utf-8")
94-
req = urllib.request.Request(
95-
f"{base_url}/v1/chat/completions", data=body,
96-
headers={"Content-Type": "application/json"}, method="POST",
97-
)
98-
t0 = time.time()
84+
def _get_model_id(base_url: str) -> Optional[str]:
85+
"""Resolve the served model id from /v1/models (the MLX verifier path)."""
9986
try:
100-
with urllib.request.urlopen(req, timeout=timeout) as resp:
101-
payload = json.loads(resp.read().decode("utf-8"))
102-
except urllib.error.HTTPError as exc:
103-
detail = exc.read().decode("utf-8", "replace")[:300]
104-
return False, "", 0, time.time() - t0, f"HTTP {exc.code}: {detail}"
105-
except Exception as exc: # noqa: BLE001 - report any transport error
106-
return False, "", 0, time.time() - t0, f"{type(exc).__name__}: {exc}"
107-
latency = time.time() - t0
87+
with urllib.request.urlopen(base_url + "/v1/models", timeout=10) as r:
88+
data = json.loads(r.read().decode("utf-8"))
89+
models = data.get("data") or []
90+
if models and isinstance(models[0], dict) and models[0].get("id"):
91+
return str(models[0]["id"])
92+
except Exception:
93+
pass
94+
return None
95+
96+
97+
def _extract(payload: Dict[str, Any]) -> Tuple[str, int]:
98+
"""Pull text + completion_tokens from a completions OR chat-completions body."""
99+
text = ""
108100
try:
109-
text = payload["choices"][0]["message"]["content"] or ""
101+
ch = payload["choices"][0]
102+
text = ch.get("text") or (ch.get("message") or {}).get("content") or ""
110103
except Exception:
111104
text = ""
112105
ctoks = 0
113106
usage = payload.get("usage") or {}
114107
if isinstance(usage, dict) and isinstance(usage.get("completion_tokens"), int):
115108
ctoks = usage["completion_tokens"]
116-
if ctoks <= 0: # fallback estimate if server omits usage
117-
ctoks = max(1, len(text.split()))
118-
return True, text, ctoks, latency, ""
109+
if ctoks <= 0:
110+
ctoks = max(1, len((text or "").split()))
111+
return text or "", ctoks
112+
113+
114+
def _post(base_url: str, path: str, body: Dict[str, Any], timeout: float):
115+
req = urllib.request.Request(
116+
base_url + path, data=json.dumps(body).encode("utf-8"),
117+
headers={"Content-Type": "application/json"}, method="POST",
118+
)
119+
with urllib.request.urlopen(req, timeout=timeout) as resp:
120+
return json.loads(resp.read().decode("utf-8"))
121+
122+
123+
def _post_generate(
124+
base_url: str, model_id: str, prompt: str, max_new_tokens: int, timeout: float,
125+
) -> Tuple[bool, str, int, float, str, str]:
126+
"""Generate via /v1/completions (raw prompt), falling back to chat.
127+
128+
Returns (ok, text, completion_tokens, latency, err, endpoint). The MLX
129+
verifier is a raw checkpoint (no chat template) so /v1/completions is the
130+
primary path; chat is a fallback for instruct builds.
131+
"""
132+
t0 = time.time()
133+
attempts = (
134+
("/v1/completions", {
135+
"model": model_id, "prompt": prompt,
136+
"max_tokens": int(max_new_tokens), "temperature": 0.0,
137+
}),
138+
("/v1/chat/completions", {
139+
"model": model_id,
140+
"messages": [{"role": "user", "content": prompt}],
141+
"max_tokens": int(max_new_tokens), "temperature": 0.0,
142+
}),
143+
)
144+
last_err = ""
145+
for path, body in attempts:
146+
try:
147+
payload = _post(base_url, path, body, timeout)
148+
except urllib.error.HTTPError as exc:
149+
last_err = f"{path} HTTP {exc.code}: {exc.read().decode('utf-8','replace')[:200]}"
150+
continue # try the next endpoint shape
151+
except Exception as exc: # noqa: BLE001
152+
return False, "", 0, time.time() - t0, f"{path}: {type(exc).__name__}: {exc}", path
153+
text, ctoks = _extract(payload)
154+
return True, text, ctoks, time.time() - t0, "", path
155+
return False, "", 0, time.time() - t0, last_err, "none"
119156

120157

121158
def _wait_for_server(
@@ -127,7 +164,7 @@ def _wait_for_server(
127164
while time.time() < deadline:
128165
if proc.poll() is not None:
129166
return False, f"server process exited early (rc={proc.returncode})"
130-
for path in ("/version", "/v1/models", "/health"):
167+
for path in ("/v1/models", "/version", "/health"):
131168
try:
132169
with urllib.request.urlopen(base_url + path, timeout=5) as r:
133170
if r.status == 200:
@@ -210,22 +247,31 @@ def _flush(status: str, **extra: Any) -> None:
210247
return 0
211248
_log(f"server ready ({detail})")
212249

250+
model_id = _get_model_id(base_url) or "default"
251+
report["served_model_id"] = model_id
252+
_log(f"served model id: {model_id}")
253+
213254
items = _build_niah_items(args.sessions, args.haystack_lines)
214255

215256
# Warmup (lazy MLX graph compile) — not measured.
216-
_post_chat(base_url, "Reply with the word ready.", 8, args.req_timeout)
257+
_post_generate(base_url, model_id, "Reply with the word ready.",
258+
8, args.req_timeout)
217259

218260
# N=1 baseline (single request decode tok/s).
219-
ok0, text0, ct0, lat0, err0 = _post_chat(
220-
base_url, items[0]["prompt"], args.max_new_tokens, args.req_timeout)
261+
ok0, text0, ct0, lat0, err0, ep0 = _post_generate(
262+
base_url, model_id, items[0]["prompt"], args.max_new_tokens,
263+
args.req_timeout)
221264
n1_tps = (ct0 / lat0) if (ok0 and lat0 > 0) else 0.0
265+
report["endpoint_used"] = ep0
266+
if not ok0:
267+
report["n1_error"] = err0
222268

223269
# N concurrent (the parallel path — continuous batching).
224-
results: List[Optional[Tuple[bool, str, int, float, str]]] = [None] * len(items)
270+
results: List[Optional[Tuple[bool, str, int, float, str, str]]] = [None] * len(items)
225271
t0 = time.time()
226272
with ThreadPoolExecutor(max_workers=len(items)) as ex:
227273
futs = {
228-
ex.submit(_post_chat, base_url, it["prompt"],
274+
ex.submit(_post_generate, base_url, model_id, it["prompt"],
229275
args.max_new_tokens, args.req_timeout): k
230276
for k, it in enumerate(items)
231277
}
@@ -234,15 +280,15 @@ def _flush(status: str, **extra: Any) -> None:
234280
try:
235281
results[k] = fut.result()
236282
except Exception as exc: # noqa: BLE001
237-
results[k] = (False, "", 0, 0.0, f"{type(exc).__name__}: {exc}")
283+
results[k] = (False, "", 0, 0.0, f"{type(exc).__name__}: {exc}", "none")
238284
wall = max(time.time() - t0, 1e-6)
239285

240286
per_session: List[Dict[str, Any]] = []
241287
hits = 0
242288
total_ctoks = 0
243289
n_ok = 0
244290
for k, (it, res) in enumerate(zip(items, results)):
245-
ok, text, ctoks, lat, err = res # type: ignore[misc]
291+
ok, text, ctoks, lat, err, _ep = res # type: ignore[misc]
246292
found = ok and (it["code"] in (text or ""))
247293
hits += 1 if found else 0
248294
total_ctoks += ctoks if ok else 0

0 commit comments

Comments
 (0)