1use std::{
24 collections::HashMap,
25 sync::{
26 atomic::{AtomicUsize, Ordering},
27 Arc, Mutex, MutexGuard,
28 },
29 time::Duration,
30};
31
32use bytes::Bytes;
33use tokio::{
34 runtime::Handle,
35 sync::{mpsc, oneshot},
36 task::JoinHandle,
37 time::timeout,
38};
39use tracing::{debug, warn};
40use ts_wire::{
41 output::{FinishDetails, PromptTopLogprobs, TopLogprobs},
42 BatchTokenIDOutSlim, ControlOutput, ControlRequest, EngineOutput, EngineRequest, RequestOutput,
43 StrOrList, TokenizedGenerateReqInput,
44};
45use zeromq::{PullSocket, RouterRecvHalf, RouterSendHalf, SocketRecv, SocketSend, ZmqMessage};
46
47use crate::{
48 config::{ClientConfig, LoadBalance, Routing},
49 error::{Error, Result, StreamError},
50 handshake::{self, EngineInfo},
51};
52
53#[derive(Debug, Clone, PartialEq)]
55pub struct OutputChunk {
56 pub output_ids: Vec<i32>,
58 pub finish_reason: Option<String>,
60 pub prompt_tokens: i64,
62 pub completion_tokens: i64,
64 pub cached_tokens: i64,
66 pub output_token_logprobs: Vec<f64>,
68 pub finish_details: Option<FinishDetails>,
71 pub spec_verify_ct: Option<i64>,
73 pub accept_draft_tokens: Option<f64>,
76 pub input_token_logprobs: Option<(Vec<Option<f64>>, Vec<i32>)>,
79 pub input_top_logprobs: Option<PromptTopLogprobs>,
82 pub output_top_logprobs: Option<TopLogprobs>,
84}
85
86impl From<&RequestOutput<'_>> for OutputChunk {
87 fn from(row: &RequestOutput<'_>) -> Self {
88 fn owned<A: Clone, B: Clone>(pair: Option<(&[A], &[B])>) -> Option<(Vec<A>, Vec<B>)> {
89 pair.map(|(a, b)| (a.to_vec(), b.to_vec()))
90 }
91 Self {
92 output_ids: row.output_ids.to_vec(),
93 finish_reason: row.finish_reason.map(str::to_owned),
94 prompt_tokens: row.prompt_tokens,
95 completion_tokens: row.completion_tokens,
96 cached_tokens: row.cached_tokens,
97 output_token_logprobs: row.output_token_logprobs.to_vec(),
98 finish_details: row.finish_details.cloned(),
99 spec_verify_ct: row.spec_verify_ct,
100 accept_draft_tokens: row.accept_draft_tokens,
101 input_token_logprobs: owned(row.input_token_logprobs),
102 input_top_logprobs: owned(row.input_top_logprobs),
103 output_top_logprobs: owned(row.output_top_logprobs),
104 }
105 }
106}
107
108#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
110pub struct EngineLoad {
111 pub num_running: i64,
113 pub num_waiting: i64,
115 pub kv_active_pages: i64,
117 pub kv_total_pages: i64,
119}
120
121type StreamItem = std::result::Result<OutputChunk, StreamError>;
122
123struct Engine {
124 info: EngineInfo,
125 identity: Bytes,
126 in_flight: AtomicUsize,
128 load: Mutex<EngineLoad>,
129}
130
131struct Pending {
132 engine: usize,
133 tx: mpsc::UnboundedSender<StreamItem>,
134}
135
136struct ControlWaiter {
138 replies: Vec<ControlOutput>,
139 done: oneshot::Sender<Vec<ControlOutput>>,
140}
141
142struct Shared {
143 engines: Vec<Engine>,
144 first_engine_index: u16,
145 routing: Routing,
146 send_timeout: Duration,
147 input: RouterSendHalf,
148 requests: Mutex<HashMap<String, Pending>>,
149 controls: Mutex<HashMap<&'static str, ControlWaiter>>,
152 control_turns: Mutex<HashMap<&'static str, Arc<tokio::sync::Mutex<()>>>>,
154 closed: Mutex<Option<StreamError>>,
156 round_robin: AtomicUsize,
157 runtime: Handle,
158}
159
160fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
161 mutex
163 .lock()
164 .unwrap_or_else(|poisoned| poisoned.into_inner())
165}
166
167impl Shared {
168 fn closed(&self) -> Option<StreamError> {
169 lock(&self.closed).clone()
170 }
171
172 fn route(&self, request: &TokenizedGenerateReqInput) -> Result<usize> {
173 let count = self.engines.len();
174 match self.routing {
175 Routing::BootstrapRoom => {
176 let room = request.bootstrap_room.ok_or_else(|| {
177 Error::InvalidRequest(
178 "disaggregated serving needs a bootstrap_room on every request".to_owned(),
179 )
180 })?;
181 Ok((room % count as u64) as usize)
182 }
183 Routing::Aggregated(policy) => {
184 if let Some(rank) = request.data_parallel_rank {
185 return usize::try_from(rank)
186 .ok()
187 .filter(|rank| *rank < count)
188 .ok_or_else(|| {
189 Error::InvalidRequest(format!(
190 "data_parallel_rank {rank} is outside 0..{count}"
191 ))
192 });
193 }
194 let start = self.round_robin.fetch_add(1, Ordering::Relaxed) % count;
195 Ok(match policy {
196 LoadBalance::RoundRobin => start,
197 LoadBalance::LeastInFlight => (0..count)
199 .map(|offset| (start + offset) % count)
200 .min_by_key(|engine| {
201 self.engines[*engine].in_flight.load(Ordering::Relaxed)
202 })
203 .expect("at least one engine"),
204 LoadBalance::MinimumCacheUsage => (0..count)
205 .map(|offset| (start + offset) % count)
206 .min_by_key(|engine| {
207 let engine = &self.engines[*engine];
208 (
209 lock(&engine.load).kv_active_pages,
210 engine.in_flight.load(Ordering::Relaxed),
211 )
212 })
213 .expect("at least one engine"),
214 })
215 }
216 }
217 }
218
219 async fn send(&self, engine: usize, frames: Vec<Bytes>) -> Result<()> {
220 let mut message = ZmqMessage::from(self.engines[engine].identity.clone());
221 for frame in frames {
222 message.push_back(frame);
223 }
224 let mut input = self.input.clone();
225 match timeout(self.send_timeout, input.send(message)).await {
226 Ok(sent) => Ok(sent?),
227 Err(_) => Err(Error::SendTimeout {
228 engine_index: self.engines[engine].info.index,
229 timeout: self.send_timeout,
230 }),
231 }
232 }
233
234 fn forget(&self, rid: &str) -> Option<usize> {
236 let pending = lock(&self.requests).remove(rid)?;
237 self.engines[pending.engine]
238 .in_flight
239 .fetch_sub(1, Ordering::Relaxed);
240 Some(pending.engine)
241 }
242
243 fn dispatch(&self, batch: &BatchTokenIDOutSlim) {
244 let slot = batch
245 .engine_index
246 .checked_sub(i64::from(self.first_engine_index))
247 .and_then(|slot| usize::try_from(slot).ok())
248 .filter(|slot| *slot < self.engines.len());
249 match slot {
250 Some(slot) => {
251 *lock(&self.engines[slot].load) = EngineLoad {
252 num_running: batch.num_running,
253 num_waiting: batch.num_waiting,
254 kv_active_pages: batch.kv_active_pages,
255 kv_total_pages: batch.kv_total_pages,
256 };
257 }
258 None => debug!(
259 engine_index = batch.engine_index,
260 "output from an unknown engine index"
261 ),
262 }
263
264 let rows = match batch.requests() {
265 Ok(rows) => rows,
266 Err(error) => {
267 warn!(%error, "dropping a malformed output batch");
268 return;
269 }
270 };
271 let mut requests = lock(&self.requests);
272 for row in rows {
273 let Some(pending) = requests.get(row.rid) else {
274 debug!(rid = row.rid, "output for a request that is not in flight");
276 continue;
277 };
278 let _ = pending.tx.send(Ok(OutputChunk::from(&row)));
281 if row.finish_reason.is_some() {
282 let pending = requests.remove(row.rid).expect("present above");
283 self.engines[pending.engine]
284 .in_flight
285 .fetch_sub(1, Ordering::Relaxed);
286 }
287 }
288 }
289
290 fn dispatch_control(&self, output: ControlOutput) {
291 let mut controls = lock(&self.controls);
292 let tag = output.tag();
293 let Some(waiter) = controls.get_mut(tag) else {
294 warn!(tag, "a control reply nobody waits for");
295 return;
296 };
297 waiter.replies.push(output);
298 if waiter.replies.len() == self.engines.len() {
299 let waiter = controls.remove(tag).expect("present above");
300 let _ = waiter.done.send(waiter.replies);
301 }
302 }
303
304 fn close(&self, reason: StreamError) {
306 lock(&self.closed).get_or_insert_with(|| reason.clone());
307 lock(&self.controls).clear();
309 let drained: Vec<Pending> = lock(&self.requests).drain().map(|(_, p)| p).collect();
310 for pending in drained {
311 self.engines[pending.engine]
312 .in_flight
313 .fetch_sub(1, Ordering::Relaxed);
314 let _ = pending.tx.send(Err(reason.clone()));
315 }
316 }
317}
318
319pub struct EngineClient {
324 shared: Arc<Shared>,
325 tasks: Vec<JoinHandle<()>>,
326}
327
328pub struct ClientListener {
334 config: ClientConfig,
335 bound: handshake::Bound,
336}
337
338impl ClientListener {
339 pub async fn bind(config: ClientConfig) -> Result<Self> {
341 let bound = handshake::bind(&config).await?;
342 Ok(Self { config, bound })
343 }
344
345 pub fn handshake_address(&self) -> &str {
347 &self.bound.handshake_address
348 }
349
350 pub async fn accept(self) -> Result<EngineClient> {
356 let config = self.config;
357 let connected = handshake::accept(self.bound, &config).await?;
358 let (input, input_recv) = connected.input.split();
359 let engines = connected
360 .engines
361 .into_iter()
362 .map(|info| Engine {
363 identity: Bytes::copy_from_slice(&ts_wire::engine_identity(info.index)),
364 info,
365 in_flight: AtomicUsize::new(0),
366 load: Mutex::new(EngineLoad::default()),
367 })
368 .collect();
369 let shared = Arc::new(Shared {
370 engines,
371 first_engine_index: config.first_engine_index,
372 routing: config.routing,
373 send_timeout: config.send_timeout,
374 input,
375 requests: Mutex::new(HashMap::new()),
376 controls: Mutex::new(HashMap::new()),
377 control_turns: Mutex::new(HashMap::new()),
378 closed: Mutex::new(None),
379 round_robin: AtomicUsize::new(0),
380 runtime: Handle::current(),
381 });
382 let tasks = vec![
383 tokio::spawn(receive_outputs(shared.clone(), connected.output)),
384 tokio::spawn(drain_input(input_recv)),
385 ];
386 Ok(EngineClient { shared, tasks })
387 }
388}
389
390impl EngineClient {
391 pub async fn connect(config: ClientConfig) -> Result<Self> {
394 ClientListener::bind(config).await?.accept().await
395 }
396
397 pub fn engines(&self) -> Vec<EngineInfo> {
399 self.shared.engines.iter().map(|e| e.info.clone()).collect()
400 }
401
402 pub fn loads(&self) -> Vec<EngineLoad> {
404 self.shared.engines.iter().map(|e| *lock(&e.load)).collect()
405 }
406
407 pub fn in_flight(&self) -> Vec<usize> {
409 self.shared
410 .engines
411 .iter()
412 .map(|e| e.in_flight.load(Ordering::Relaxed))
413 .collect()
414 }
415
416 pub async fn generate(&self, request: TokenizedGenerateReqInput) -> Result<RequestStream> {
422 let rid = match &request.rid {
423 Some(StrOrList::One(rid)) => rid.clone(),
424 _ => {
425 return Err(Error::InvalidRequest(
426 "a generate request needs a single string rid".to_owned(),
427 ))
428 }
429 };
430 if let Some(reason) = self.shared.closed() {
431 return Err(Error::Unavailable(reason));
432 }
433 let engine = self.shared.route(&request)?;
434 let (tx, rx) = mpsc::unbounded_channel();
435 {
436 let mut requests = lock(&self.shared.requests);
439 if requests.contains_key(&rid) {
440 return Err(Error::DuplicateRequestId(rid));
441 }
442 requests.insert(rid.clone(), Pending { engine, tx });
443 }
444 self.shared.engines[engine]
445 .in_flight
446 .fetch_add(1, Ordering::Relaxed);
447 let frames = EngineRequest::Add(Box::new(request)).to_frames();
448 if let Err(error) = self.shared.send(engine, frames).await {
449 self.shared.forget(&rid);
450 return Err(error);
451 }
452 Ok(RequestStream {
453 shared: self.shared.clone(),
454 rid,
455 rx,
456 done: false,
457 })
458 }
459
460 pub async fn control(&self, request: ControlRequest) -> Result<Vec<ControlOutput>> {
465 let tag = request.reply_tag();
466 let turn = lock(&self.shared.control_turns)
467 .entry(tag)
468 .or_default()
469 .clone();
470 let _turn = turn.lock().await;
471 if let Some(reason) = self.shared.closed() {
472 return Err(Error::Unavailable(reason));
473 }
474 let (done, replies) = oneshot::channel();
475 lock(&self.shared.controls).insert(
476 tag,
477 ControlWaiter {
478 replies: Vec::new(),
479 done,
480 },
481 );
482 let frames = EngineRequest::Control(Box::new(request)).to_frames();
483 for engine in 0..self.shared.engines.len() {
484 if let Err(error) = self.shared.send(engine, frames.clone()).await {
485 lock(&self.shared.controls).remove(tag);
486 return Err(error);
487 }
488 }
489 replies.await.map_err(|_| {
490 Error::Unavailable(self.shared.closed().unwrap_or(StreamError::ClientClosed))
491 })
492 }
493
494 pub async fn abort(&self, rid: &str) -> Result<bool> {
497 let engine = match lock(&self.shared.requests).get(rid) {
498 Some(pending) => pending.engine,
499 None => return Ok(false),
500 };
501 let frames = EngineRequest::Abort(vec![rid.to_owned()]).to_frames();
502 self.shared.send(engine, frames).await?;
503 Ok(true)
504 }
505}
506
507impl Drop for EngineClient {
508 fn drop(&mut self) {
509 for task in &self.tasks {
510 task.abort();
511 }
512 self.shared.close(StreamError::ClientClosed);
513 }
514}
515
516pub struct RequestStream {
518 shared: Arc<Shared>,
519 rid: String,
520 rx: mpsc::UnboundedReceiver<StreamItem>,
521 done: bool,
522}
523
524impl RequestStream {
525 pub fn rid(&self) -> &str {
527 &self.rid
528 }
529
530 pub async fn next(&mut self) -> Option<StreamItem> {
533 if self.done {
534 return None;
535 }
536 let item = self.rx.recv().await;
537 self.done = match &item {
538 Some(Ok(chunk)) => chunk.finish_reason.is_some(),
539 Some(Err(_)) | None => true,
540 };
541 item
542 }
543}
544
545impl Drop for RequestStream {
546 fn drop(&mut self) {
547 if self.done {
548 return;
549 }
550 let Some(engine) = self.shared.forget(&self.rid) else {
551 return;
552 };
553 let shared = self.shared.clone();
554 let frames = EngineRequest::Abort(vec![self.rid.clone()]).to_frames();
555 let rid = self.rid.clone();
556 self.shared.runtime.spawn(async move {
557 if let Err(error) = shared.send(engine, frames).await {
558 warn!(rid, %error, "failed to abort a dropped request");
559 }
560 });
561 }
562}
563
564async fn receive_outputs(shared: Arc<Shared>, mut output: PullSocket) {
565 loop {
566 let message = match output.recv().await {
567 Ok(message) => message,
568 Err(error) => {
569 warn!(%error, "output socket failed");
572 shared.close(StreamError::Transport(error.to_string()));
573 return;
574 }
575 };
576 match EngineOutput::from_frames(&message.into_vec()) {
577 Ok(EngineOutput::Batch(batch)) => shared.dispatch(&batch),
578 Ok(EngineOutput::Control(output)) => shared.dispatch_control(*output),
579 Ok(EngineOutput::Dead) => {
580 warn!("an engine shut down");
581 shared.close(StreamError::EngineDead);
582 return;
583 }
584 Err(error) => warn!(%error, "dropping a malformed output message"),
585 }
586 }
587}
588
589async fn drain_input(mut input: RouterRecvHalf) {
592 loop {
593 match input.recv().await {
594 Ok(message) => warn!(
595 frames = message.len(),
596 "unexpected message on the input socket"
597 ),
598 Err(error) => {
599 debug!(%error, "input socket receive ended");
600 return;
601 }
602 }
603 }
604}