@@ -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
121158def _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