@@ -614,6 +614,24 @@ def last(self, value):
614614 tl = self .__dict__ .setdefault ("_tl" , threading .local ())
615615 tl .last = value
616616
617+ # `lp_lines`: the LP lines of the tokens of the request being read (logprobs), taken by LogprobEntries on the
618+ # thread that reads the request's tokens. With batch slots several requests run at once: one deque per thread.
619+ @property
620+ def lp_lines (self ):
621+ if self .batch :
622+ tl = self .__dict__ .setdefault ("_tl" , threading .local ())
623+ if not hasattr (tl , "lp_lines" ):
624+ tl .lp_lines = collections .deque ()
625+ return tl .lp_lines
626+ return self .__dict__ .setdefault ("_lp_lines" , collections .deque ())
627+
628+ @lp_lines .setter
629+ def lp_lines (self , value ):
630+ if self .batch :
631+ self .__dict__ .setdefault ("_tl" , threading .local ()).lp_lines = value
632+ else :
633+ self .__dict__ ["_lp_lines" ] = value
634+
617635 def __init__ (self , exe : str , args : list [str ], cwd : str | None = None , log : str | None = None ,
618636 env : dict | None = None , lazy : bool = False , cpus : list [int ] | None = None ):
619637 self .spawn = (exe , list (args ), cwd , log , env , False , cpus ) # to start it again after it died (issue #27)
@@ -1035,18 +1053,34 @@ def _send(self, text: str):
10351053 except (OSError , AttributeError ): # the pipe is gone (or a restart's close() took the process):
10361054 raise EngineDied (f"the engine stopped unexpectedly (exit code { self .exit_code ()} )" ) from None
10371055
1038- def _control (self , cancel , on_token , stop_when = None ):
1056+ def _control (self , cancel , on_token , stop_when = None , want_lp = False ):
10391057 """Reads the control lines of the request on them (GEN / BGEN), yielding None heartbeats. Calls on_token(id)
10401058 for each `T`; returns ("done", None) at DONE, or ("badm", continues) at BADM (after DONE). `stop_when()`
1041- true sends STOP once (the request is then read to its DONE)."""
1059+ true sends STOP once (the request is then read to its DONE). `want_lp` (logprobs): a `T` is held until the
1060+ next line, and its `LP` line (the engine writes it right after the token, in the same flush) goes to lp_lines
1061+ before the token is passed on, so whoever takes the token finds it; any other line, or 0.5 s of silence,
1062+ passes the token on first (an engine without LP lines)."""
10421063 stopped = False
10431064 beat = time .monotonic ()
1065+ held = None
1066+
1067+ def pass_on (t ):
1068+ nonlocal stopped
1069+ on_token (t )
1070+ if not stopped and (cancel .is_set () or (stop_when is not None and stop_when ())):
1071+ self ._send ("STOP" )
1072+ stopped = True
10441073 while True :
10451074 try :
10461075 # A client can leave before the first PP/token. Poll its Event as often as _take_control does;
10471076 # the ten-second HTTP heartbeat is independent of how promptly STOP reaches the engine.
10481077 line = self .lines .get (timeout = 0.5 )
10491078 except queue .Empty :
1079+ if held is not None :
1080+ t , held = held , None
1081+ pass_on (t )
1082+ yield False
1083+ continue
10501084 if cancel .is_set () and not stopped :
10511085 self ._send ("STOP" )
10521086 stopped = True
@@ -1059,11 +1093,19 @@ def _control(self, cancel, on_token, stop_when=None):
10591093 raise EngineDied (f"the engine stopped unexpectedly (exit code { self .exit_code ()} )" )
10601094 if not line .startswith (("PP " , "INFO" )):
10611095 btrace ("ctl<" , self ._ctl_mode , line .strip ()[:60 ])
1096+ if held is not None :
1097+ t , held = held , None
1098+ if line .startswith ("LP " ):
1099+ self .lp_lines .append ((t , line ))
1100+ pass_on (t )
1101+ yield False # a token is pending (not a heartbeat)
1102+ if line .startswith ("LP " ):
1103+ continue
10621104 if line .startswith ("T " ):
1063- on_token ( int ( line [ 2 :]))
1064- if not stopped and ( cancel . is_set () or ( stop_when is not None and stop_when ())):
1065- self . _send ( "STOP" )
1066- stopped = True
1105+ if want_lp :
1106+ held = int ( line [ 2 :])
1107+ continue
1108+ pass_on ( int ( line [ 2 :]))
10671109 yield False # a token is pending (not a heartbeat)
10681110 elif line .startswith ("PP " ):
10691111 f = line .split ()
@@ -1255,6 +1297,8 @@ def generate_batched(self, ids, max_new, sampling, cancel, embeddings=None):
12551297 self .progress , self .progress_ms , self .reused = None , 0 , 0
12561298 born = self .gen # the engine process this request is sent to
12571299 keys = self .sampling_keys (sampling or {})
1300+ want_lp = top_logprobs (sampling or {}) is not None
1301+ self .lp_lines = collections .deque () # this request's (per thread with batch slots)
12581302 out : list [int ] = []
12591303 pending : list [int ] = []
12601304 prompt , left = list (ids ), int (max_new )
@@ -1287,7 +1331,7 @@ def generate_batched(self, ids, max_new, sampling, cancel, embeddings=None):
12871331 def others ():
12881332 with self .slot_cv :
12891333 return self .waiting > 0
1290- for x in self ._control (cancel , pending .append , stop_when = others ):
1334+ for x in self ._control (cancel , pending .append , stop_when = others , want_lp = want_lp ):
12911335 while pending :
12921336 t = pending .pop (0 )
12931337 out .append (t )
@@ -1392,7 +1436,7 @@ def others():
13921436 self ._ctl_mode , self ._ctl_result , self ._yielded = "batch" , None , None
13931437 self ._ctl_erred = False
13941438 asked = False
1395- for x in self ._control (cancel , pending .append ):
1439+ for x in self ._control (cancel , pending .append , want_lp = want_lp ):
13961440 while pending :
13971441 t = pending .pop (0 )
13981442 out .append (t )
@@ -5370,6 +5414,11 @@ def _openai(self, req):
53705414 svc .load ()
53715415 max_req = max_new = int (req .get ("max_completion_tokens" ) or req .get ("max_tokens" ) or 0 ) # 0/-1: the rest
53725416 bias_api .validate_request (req , svc .engine , len (getattr (svc .tok , "tokens" , ())) or None )
5417+ if top_logprobs (req ) is not None and getattr (svc .engine , "batch" , 0 ) and max_req != 1 :
5418+ # a batch slot's BT lines carry no LP lines: past its first token a request would get partial or
5419+ # misplaced entries. One token (a typed decision) is read on the control lines and has its LP line.
5420+ raise ValueError ('logprobs: with "parallel" (batch slots) only for max_tokens 1; '
5421+ 'run without "parallel" for longer replies' )
53735422 use_mcp = req .get ("strata_mcp" ) is True and svc .mcp is not None # the web app's opt-in (serve/mcp.py)
53745423 own = {t .get ("name" ) for t in tools or [] if isinstance (t , dict )} # #592: a second line of defence
53755424 if use_mcp :
0 commit comments