tract_core/lanes.rs
1//! Serving many streams through one state.
2//!
3//! A prepared model serves one stream at a time: `spawn()` hands out a state,
4//! and each `run()` on it is one turn of that stream. [`LanedRunnable::wrap`]
5//! makes one state serve many streams at once, by batching the turns that
6//! happen to be ready into a single `run()` on a single state. Callers see
7//! nothing of it: they still spawn a state each and run it per turn.
8//!
9//! This is the one corner of `core` where threads, queues and a promise per
10//! caller appear, so its plumbing carries more comment than the rest of the
11//! crate: the types are small, and what they mean is where the bugs are.
12//!
13//! # Two batch axes, and the words for them
14//!
15//! `doc/lexicon.md` is the reference; the four words this module lives on:
16//!
17//! - **lane** -- where one stream's state sits inside the shared state: axis 0
18//! of a laned op state's buffers, of extent `max_lanes`, addressed through
19//! the turn's [`Seating`].
20//! - **seat** -- a position in one turn's batch: axis 0 of the turn's input and
21//! output tensors, of extent the turn's occupancy.
22//! - **turn** -- one `run()` on the state. It has one seating, and its seats
23//! are the streams it serves.
24//! - **call** -- one `run()` on a [`LanedStateHandle`]. Usually one seat of one
25//! turn, but a caller feeding several at once asks for several seats, and is
26//! answered over as many turns as they took to seat.
27//!
28//! One lane takes at most one seat per turn: a stream's state is sequential.
29//!
30//! # The actors
31//!
32//! ```text
33//! caller thread caller thread
34//! (stream A) (stream B)
35//! | |
36//! LanedStateHandle LanedStateHandle
37//! lane 0, cloned lane 1, cloned
38//! | |
39//! | Request::{Spawn, Call, Drop} |
40//! +---------------+-----------------+
41//! | one mpsc queue, cloned per handle
42//! v
43//! +--------------------------------------------+
44//! | worker thread "tract-lanes" |
45//! | |
46//! | Worker state the one state, laned |
47//! | lanes which of them are taken |
48//! | queued seats waiting, and the |
49//! | calls they answer |
50//! +--------------------------------------------+
51//! |
52//! | one answer channel per call
53//! v
54//! back to the callers
55//! ```
56//!
57//! The worker owns the state and the [`LaneTable`] both, and is the only thread
58//! which touches either. That is not tidiness: taking a lane **resets** it,
59//! which writes the state -- device memory for a state on a GPU -- so it has to
60//! happen where the state lives. Hence a handle asks for a lane rather than
61//! taking one, and `Lease` only knows how to send it back.
62//!
63//! # The life of a handle, in requests
64//!
65//! - `Request::Spawn` -- `LanedRunnable::spawn` asks for a lane and blocks
66//! until the worker resets one and answers, or until it answers that every
67//! lane is taken.
68//! - `Request::Call` -- `LanedStateHandle::run` sends its inputs and a
69//! one-shot channel, then blocks on that channel.
70//! - `Request::Drop` -- the last clone of a handle dropped sends its lane
71//! back, and the next stream can have it.
72//!
73//! # The life of a turn
74//!
75//! 1. The worker blocks until a request arrives. If it put a seat in the queue,
76//! the turn **lingers** for [`TRACT_TURN_LINGER_US`], so that streams whose
77//! pulses land within a hair of each other share a turn instead of taking
78//! one each. Zero by default.
79//! 2. Everything else pending is drained, so the turn sees every seat ready.
80//! 3. `fill` seats the head of the queue: at most [`TRACT_MAX_SEATS`] seats, at
81//! most one per lane.
82//! 4. `run_turn` stacks the batched inputs along axis 0 in seat order, checks
83//! that the shared ones agree across seats, publishes the seating, and runs
84//! the state **once**.
85//! 5. Its outputs are sliced back per seat, borrowed lanes go back to the
86//! table, and each seat is handed to its call's `Completer`, which answers
87//! the caller once its last seat has landed.
88//!
89//! A turn that fails fails every seat of it, and a seat that fails fails its
90//! whole call and drops that call's seats still queued.
91//!
92//! # A call asking for several seats
93//!
94//! A beam decoder hands a stateless model its k hypotheses in one `run()`. Such
95//! a call **explodes** into one `Seat` per slice of its batched inputs; the
96//! queue is therefore a queue of seats, not of calls, and a call wider than the
97//! free lanes is split at the turn boundary rather than held until it fits
98//! whole -- so a wide call cannot starve the one-seat calls behind it. Its first
99//! seat sits in its caller's own lane and the rest **borrow** free lanes, which
100//! resets them, so no seat ever reads what another caller left.
101//!
102//! With `max_lanes` 4, A holding lane 0 and B lane 1, A calling for 5 seats and
103//! B for one:
104//!
105//! ```text
106//! A: run([5, ..]) B: run([1, ..])
107//! | |
108//! | explode | explode
109//! v v
110//! A0 A1 A2 A3 A4 B0
111//! | |
112//! +----------------+-------------------+
113//! v
114//! queue: A0 A1 A2 A3 A4 B0
115//!
116//! turn 1 lane 0 lane 1 lane 2 lane 3
117//! A0 -> its own lane [ A0 ] [ B0 ] [ A1 ] [ A2 ]
118//! A1 -> borrows lane 2 | | | |
119//! A2 -> borrows lane 3 +-------+---+---+-------+
120//! A3 -> nothing free, waits | one run(), occupancy 4
121//! A4 -> waits v
122//! B0 -> its own lane B answered; A has 3 of 5 seats
123//!
124//! turn 2 lane 0 lane 1 lane 2 lane 3
125//! A3 -> its own lane [ A3 ] -- [ A4 ] --
126//! A4 -> borrows lane 2 | |
127//! +-------+-------+
128//! | one run(), occupancy 2
129//! v
130//! A's 5 seats stack back into one [5, ..]
131//! ```
132//!
133//! A one-seat call pays nothing for any of this: `explode` does not slice it and
134//! `assemble` hands its outputs straight back.
135//!
136//! # What a laned model must satisfy
137//!
138//! - **The batch axis is axis 0**, on at least one input and one output, and it
139//! is one symbol for all of them. It is the only position where a seat's
140//! values are a contiguous run, which is what makes stacking a memcpy and
141//! slicing a view, and the only one whose seats are independent under a
142//! liquid schedule. A model wanting it elsewhere gets it moved by a graph
143//! edit, never by a per-turn transpose here.
144//! - **An input or output whose axis 0 is not a symbol is shared**: one value of
145//! it serves the whole turn, so seats disagreeing about it fail the turn, and
146//! a shared output is handed back to every seat.
147//! - **Axis 0 of every stateful node** is that batch axis. `wrap` walks the I/O
148//! facts and resets every lane once, which fails a state that cannot serve
149//! several streams; the interior is otherwise on trust.
150//!
151//! # Traps
152//!
153//! - `Seat::lane` is the lane the seat **sits in**, not the lane its caller
154//! holds: `fill` overwrites it when it borrows, or a turn ends up seating one
155//! lane twice.
156//! - The knobs are read at `wrap` and are process-wide, so tests which set
157//! [`TRACT_TURN_LINGER_US`] have to serialize.
158//! - Dropping the runnable closes the queue and **joins** the worker. The worker
159//! holds per-thread device state whose destructors must not run against
160//! libraries already tearing themselves down.
161//!
162//! # Not here
163//!
164//! Admission is not a policy yet: `spawn` fails when every lane is taken rather
165//! than waiting for one. A caller holding a lane for the life of a handle is
166//! what makes that felt, and what a server wants of it is still open.
167
168use std::collections::{HashMap, VecDeque};
169use std::fmt::Debug;
170use std::sync::Mutex;
171use std::sync::atomic::{AtomicU64, Ordering};
172use std::sync::mpsc::{Receiver, Sender, channel};
173use std::thread;
174use std::time::Duration;
175
176use crate::internal::*;
177
178/// The lanes of one laned state: which are taken, and which of them a turn
179/// seats.
180///
181/// Plain data. Taking a lane does not touch the state's buffers, and clearing
182/// what a stream left in a lane it gave up is the table's caller's, since it
183/// writes the state -- device memory for a state on a GPU -- and must run where
184/// the state lives. So a lane handed to a new stream carries the previous one's
185/// history until that caller resets it.
186#[derive(Debug, Clone, PartialEq, Eq)]
187pub struct LaneTable {
188 /// One flag per lane, true while a stream holds it. A lane's index is its
189 /// [`LaneId`], and the length is the state's fixed lane count.
190 taken: Vec<bool>,
191}
192
193impl LaneTable {
194 pub fn new(max_lanes: usize) -> TractResult<LaneTable> {
195 ensure!(max_lanes > 0, "A laned state needs at least one lane");
196 Ok(LaneTable { taken: vec![false; max_lanes] })
197 }
198
199 /// The extent of the lane axis of the state's per-lane buffers, fixed for
200 /// the life of the state.
201 pub fn max_lanes(&self) -> usize {
202 self.taken.len()
203 }
204
205 pub fn taken(&self) -> usize {
206 self.taken.iter().filter(|t| **t).count()
207 }
208
209 /// The lowest free lane, `None` when every lane is taken -- whether that
210 /// blocks the new stream or fails it is the caller's policy. Lowest first,
211 /// so that a turn seating every lane seats a run of consecutive lanes.
212 pub fn take(&mut self) -> Option<LaneId> {
213 let lane = self.taken.iter().position(|t| !t)?;
214 self.taken[lane] = true;
215 Some(LaneId(lane))
216 }
217
218 /// Hand `lane` back, for [`LaneTable::take`] to give to another stream.
219 pub fn give_back(&mut self, lane: LaneId) -> TractResult<()> {
220 ensure!(self.is_taken(lane), "Lane {} is not taken, so it can not be given back", lane.0);
221 self.taken[lane.0] = false;
222 Ok(())
223 }
224
225 pub fn is_taken(&self, lane: LaneId) -> bool {
226 self.taken.get(lane.0).copied().unwrap_or(false)
227 }
228
229 /// Seat `lanes`, in that order: seat `ix` of the coming turn carries the
230 /// `ix`th of them. Every one must be taken, so that a stream which ended
231 /// can not be seated by a stale handle of it.
232 pub fn seat(&self, lanes: impl IntoIterator<Item = LaneId>) -> TractResult<Seating> {
233 let lanes: Vec<LaneId> = lanes.into_iter().collect();
234 for lane in &lanes {
235 ensure!(self.is_taken(*lane), "Seating lane {}, which no stream took", lane.0);
236 }
237 Seating::new(self.max_lanes(), lanes)
238 }
239}
240
241crate::declare_knob!(
242 TRACT_MAX_SEATS,
243 usize,
244 256,
245 "Most streams a laned runtime serves in one turn, clamped to the state's lanes."
246);
247
248crate::declare_knob!(
249 TRACT_TURN_LINGER_US,
250 usize,
251 0,
252 "How long a laned runtime waits for more streams once one is ready to run."
253);
254
255/// A model prepared to serve many streams at once: one state, one lane per
256/// stream, and turns seating whoever is ready.
257///
258/// `spawn` hands out a [`LanedStateHandle`] per stream, each holding a lane, and
259/// every `run` on a handle is a request to the worker thread which owns the
260/// state and the [`LaneTable`] both. The worker takes the turns queued at that
261/// moment, at most one per lane and at most [`TRACT_MAX_SEATS`] of them,
262/// concatenates their inputs along axis 0, publishes the seating and runs the
263/// state once, then hands each stream back its own seat.
264///
265/// A stream feeds one seat per turn: axis 0 carries streams, not data. Inputs and
266/// outputs whose axis 0 is a symbol are the batched ones; the rest are shared,
267/// so one value of such an input serves the whole turn and every seat must feed
268/// the same one, and such an output is handed back to every stream.
269#[derive(Clone)]
270pub struct LanedRunnable {
271 shared: Arc<Shared>,
272}
273
274struct Shared {
275 /// [`std::sync::mpsc::Sender`] is not `Sync`, and a `Runnable` is: handles
276 /// take their own clone of it, under the lock, once. `None` once the
277 /// runnable is being dropped, which is what closes the queue.
278 requests: Mutex<Option<Sender<Request>>>,
279 /// Joined when the runnable is dropped, so the worker is gone before
280 /// whatever the caller does next.
281 worker: Mutex<Option<thread::JoinHandle<()>>>,
282 /// The one-stream model the worker spawned its state from.
283 inner: Arc<dyn Runnable>,
284 /// `inner`'s model and plan, carried so that a laned runnable answers
285 /// [`Runnable::typed_model`] and [`Runnable::typed_plan`] like any other.
286 model: Option<Arc<TypedModel>>,
287 plan: Option<Arc<TypedSimplePlan>>,
288 /// The symbol axis 0 of the batched tensors carries: the turn's occupancy,
289 /// never a stream's own shapes.
290 batch: Symbol,
291 /// Lanes the state was reset for at `wrap`, hence the most streams that can
292 /// hold a handle at once and the widest a turn can be.
293 max_lanes: usize,
294 /// Turns and seats served, shared with the worker which is what counts them.
295 counts: Arc<Counts>,
296}
297
298/// The worker owns per-thread device state -- a CUDA stream, its cuBLAS and
299/// cuDNN handles -- whose destructors run as the thread exits. Nothing joined
300/// it before, so a process that returned from `main` while the worker was
301/// still winding down ran those destructors against libraries already tearing
302/// themselves down in their own `atexit` handlers, and segfaulted in
303/// `cudnnDestroy` about one run in fifteen. Closing the queue is what stops
304/// the worker, so the sender goes first and the join waits for at most the
305/// turn in flight.
306impl Drop for Shared {
307 fn drop(&mut self) {
308 if let Ok(mut requests) = self.requests.lock() {
309 requests.take();
310 }
311 let worker = self.worker.lock().ok().and_then(|mut worker| worker.take());
312 if let Some(worker) = worker {
313 let _ = worker.join();
314 }
315 }
316}
317
318/// What the worker has served, for whoever tunes the turn policy: mean
319/// occupancy is `seats / turns`.
320#[derive(Debug, Default)]
321struct Counts {
322 /// Turns run since the model was prepared.
323 turns: AtomicU64,
324 /// Seats filled over those turns, so `seats / turns` is mean occupancy.
325 seats: AtomicU64,
326}
327
328impl LanedRunnable {
329 /// Serve `max_lanes` streams through `inner`, which must be prepared from a
330 /// model carrying a batch axis: at least one input and one output with a
331 /// symbol on axis 0, and one symbol for all of them.
332 pub fn wrap(inner: Arc<dyn Runnable>, max_lanes: usize) -> TractResult<LanedRunnable> {
333 let model = inner.typed_model().cloned();
334 let plan = inner.typed_plan().cloned();
335 let mut symbols: Vec<Symbol> = vec![];
336 let mut facts: Vec<(String, TypedFact)> = vec![];
337 let mut batch_in: Vec<bool> = vec![];
338 for ix in 0..inner.input_count() {
339 let fact = inner.input_fact(ix)?;
340 let symbol = batch_symbol(fact);
341 batch_in.push(symbol.is_some());
342 symbols.extend(symbol);
343 facts.push((format!("input {ix}"), fact.clone()));
344 }
345 let mut batch_out: Vec<bool> = vec![];
346 for ix in 0..inner.output_count() {
347 let fact = inner.output_fact(ix)?;
348 let symbol = batch_symbol(fact);
349 batch_out.push(symbol.is_some());
350 symbols.extend(symbol);
351 facts.push((format!("output {ix}"), fact.clone()));
352 }
353 symbols.sort();
354 symbols.dedup();
355 if symbols.len() != 1 {
356 bail!(off_axis_zero(&facts, &symbols));
357 }
358 ensure!(batch_out.iter().any(|b| *b), "A laned model must batch one output at least");
359 let batch = symbols.remove(0);
360 let counts = Arc::new(Counts::default());
361 let max_seats = TRACT_MAX_SEATS.get().min(max_lanes);
362 let linger = Duration::from_micros(TRACT_TURN_LINGER_US.get() as u64);
363 let lanes = LaneTable::new(max_lanes)?;
364 let (requests, queue) = channel::<Request>();
365 let (spawned, ready) = channel::<TractResult<()>>();
366 let worker_counts = counts.clone();
367 let worker_inner = inner.clone();
368 let worker_thread = thread::Builder::new().name("tract-lanes".into()).spawn(move || {
369 let state = match worker_inner.spawn().and_then(|mut state| {
370 let every: Vec<LaneId> = (0..max_lanes).map(LaneId).collect();
371 state.reset_lanes(&every).context("Preparing a laned model")?;
372 Ok(state)
373 }) {
374 Ok(state) => {
375 let _ = spawned.send(Ok(()));
376 state
377 }
378 Err(e) => {
379 let _ = spawned.send(Err(e));
380 return;
381 }
382 };
383 Worker {
384 state,
385 lanes,
386 queued: Queue::default(),
387 batch_in,
388 batch_out,
389 max_seats,
390 linger,
391 counts: worker_counts,
392 }
393 .work(queue);
394 })?;
395 ready.recv().map_err(|_| format_err!("The laned worker died spawning the state"))??;
396 Ok(LanedRunnable {
397 shared: Arc::new(Shared {
398 requests: Mutex::new(Some(requests)),
399 worker: Mutex::new(Some(worker_thread)),
400 inner,
401 model,
402 plan,
403 batch,
404 max_lanes,
405 counts,
406 }),
407 })
408 }
409
410 pub fn max_lanes(&self) -> usize {
411 self.shared.max_lanes
412 }
413
414 /// The model as it was prepared, serving one stream at a time: what a turn
415 /// of one seat has to agree with.
416 pub fn inner(&self) -> &Arc<dyn Runnable> {
417 &self.shared.inner
418 }
419
420 /// The symbol axis 0 of the batched tensors carries. A stream feeds one seat
421 /// per turn, so it stands for the turn's occupancy, never for a stream's
422 /// own shapes.
423 pub fn batch_symbol(&self) -> &Symbol {
424 &self.shared.batch
425 }
426
427 /// Turns run and seats filled since the model was prepared: how wide the
428 /// turns the queue actually offers are.
429 pub fn turns_and_seats(&self) -> (u64, u64) {
430 (
431 self.shared.counts.turns.load(Ordering::Relaxed),
432 self.shared.counts.seats.load(Ordering::Relaxed),
433 )
434 }
435
436 fn request(&self) -> TractResult<Sender<Request>> {
437 self.shared
438 .requests
439 .lock()
440 .map_err(|_| format_err!("Poisoned laned sender"))?
441 .clone()
442 .context("The laned runnable is gone")
443 }
444}
445
446/// The symbol axis 0 of `fact` carries, or `None` for a tensor every seat
447/// shares. A stored fact can claim a symbol on an axis of extent one, so this
448/// says how the caller talks, not what the graph does with it.
449fn batch_symbol(fact: &TypedFact) -> Option<Symbol> {
450 match fact.shape.dims().first() {
451 Some(TDim::Sym(sym)) => Some(sym.clone()),
452 _ => None,
453 }
454}
455
456/// Why `facts` do not carry one batch symbol on axis 0: where the symbols they
457/// do carry sit, and that putting one on axis 0 is a graph edit.
458fn off_axis_zero(facts: &[(String, TypedFact)], symbols: &[Symbol]) -> TractError {
459 let elsewhere: Vec<String> = facts
460 .iter()
461 .filter_map(|(what, fact)| {
462 let dims = fact.shape.dims();
463 let axis = dims.iter().skip(1).position(|dim| matches!(dim, TDim::Sym(_)))? + 1;
464 Some(format!("{what} carries {} on axis {axis}", dims[axis]))
465 })
466 .collect();
467 let elsewhere = elsewhere.join(", ");
468 if symbols.is_empty() {
469 let found = if elsewhere.is_empty() {
470 "none of them carries a symbol at all".to_string()
471 } else {
472 elsewhere
473 };
474 format_err!(
475 "A laned model carries its batch symbol on axis 0 of the inputs and outputs it \
476 batches, and {found}. Axis 0 is where a seat's values are a contiguous run, so move \
477 the batch there as a graph edit -- Batchify, or an AddAxis/MoveAxis the optimiser \
478 can absorb -- rather than have every turn transpose"
479 )
480 } else {
481 let symbols: Vec<String> = symbols.iter().map(|s| s.to_string()).collect();
482 format_err!(
483 "A laned model carries one batch symbol on axis 0, this one carries {}. One symbol \
484 has to stand for the whole turn's occupancy, so share it across the batched inputs \
485 and outputs in the export",
486 symbols.join(" and ")
487 )
488 }
489}
490
491impl Debug for LanedRunnable {
492 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
493 write!(f, "LanedRunnable({} lanes)", self.shared.max_lanes)
494 }
495}
496
497impl Runnable for LanedRunnable {
498 fn spawn(&self) -> TractResult<Box<dyn State>> {
499 let requests = self.request()?;
500 let (taken, lane) = channel();
501 requests
502 .send(Request::Spawn(taken))
503 .map_err(|_| format_err!("The laned worker is gone"))?;
504 let lane = lane.recv().map_err(|_| format_err!("The laned worker dropped a lane"))??;
505 Ok(Box::new(LanedStateHandle {
506 lease: Arc::new(Lease { lane, requests }),
507 runnable: self.clone(),
508 }))
509 }
510
511 fn typed_plan(&self) -> Option<&Arc<TypedSimplePlan>> {
512 self.shared.plan.as_ref()
513 }
514
515 fn typed_model(&self) -> Option<&Arc<TypedModel>> {
516 self.shared.model.as_ref()
517 }
518}
519
520/// One stream's view of a [`LanedRunnable`]: the lane it holds, and the queue to
521/// the worker. Cloning it shares the lane -- clones are the same stream, and the
522/// lane goes back to the table once the last of them is dropped.
523#[derive(Clone, Debug)]
524pub struct LanedStateHandle {
525 /// The lane this stream holds, shared by the handle's clones: the last of
526 /// them dropped is what gives the lane back.
527 lease: Arc<Lease>,
528 /// The runnable the handle came from, for [`State::runnable`].
529 runnable: LanedRunnable,
530}
531
532#[derive(Debug)]
533struct Lease {
534 /// The lane the worker handed this stream at spawn.
535 lane: LaneId,
536 /// Where to send the lane back, which is all `Drop` needs.
537 requests: Sender<Request>,
538}
539
540impl Drop for Lease {
541 fn drop(&mut self) {
542 let _ = self.requests.send(Request::Drop(self.lane));
543 }
544}
545
546impl State for LanedStateHandle {
547 fn run(&mut self, inputs: TVec<TValue>) -> TractResult<TVec<TValue>> {
548 let (done, outputs) = channel();
549 self.lease
550 .requests
551 .send(Request::Call(Call { leased: self.lease.lane, inputs, done }))
552 .map_err(|_| format_err!("The laned worker is gone"))?;
553 outputs.recv().map_err(|_| format_err!("The laned worker dropped a turn"))?
554 }
555
556 fn runnable(&self) -> &dyn Runnable {
557 &self.runnable
558 }
559}
560
561/// What a handle asks the worker for, one variant per event of the handle's
562/// life: a lane when it is spawned, a turn per call, and its lane back when it
563/// is dropped.
564enum Request {
565 /// A new stream wants a lane; the worker answers with one, reset, or with
566 /// the error that every lane is taken.
567 Spawn(Sender<TractResult<LaneId>>),
568 /// A stream wants a turn, or several at once.
569 Call(Call),
570 /// A stream is over and its lane goes back to the table.
571 Drop(LaneId),
572}
573
574/// One `run()` on a handle. Its inputs carry one seat or several, and it is
575/// answered once every one of them has been served -- over as many turns as
576/// the free lanes took to seat them all.
577struct Call {
578 /// The lane the caller holds for the life of its handle, which this call's
579 /// first seat sits in. It is a reservation, not a home: a call asking for
580 /// several seats borrows the rest.
581 leased: LaneId,
582 /// What the caller fed, batched inputs still stacked as they came.
583 inputs: TVec<TValue>,
584 /// Where the assembled answer goes; the caller blocks on the other end.
585 done: Sender<TractResult<TVec<TValue>>>,
586}
587
588/// One seat of one call, waiting for a lane: the lane it sits in -- its
589/// caller's own until a turn borrows another for it -- its place in the call,
590/// and the slice of the call's inputs it feeds.
591struct Seat {
592 /// The call this seat answers, keying its [`Completer`].
593 call: u64,
594 /// The lane the seat sits in: the call's leased one, overwritten by `fill`
595 /// with a borrowed one when that lane already took a seat this turn.
596 lane: LaneId,
597 /// Its place in the call, so the answer stacks back in the order asked.
598 ix: usize,
599 /// One seat's slice of the call's inputs, shared inputs whole.
600 inputs: TVec<TValue>,
601}
602
603/// The half of a call's answer the worker holds: its seats' outputs as they
604/// land, in the order it asked for them, and where to send them once the last
605/// of them has. The caller holds the other half and blocks on it.
606struct Completer {
607 /// One slot per seat the call asked for, filled as its seats land -- over
608 /// several turns when the call was wider than the free lanes.
609 served: Vec<Option<TVec<TValue>>>,
610 /// The call's own answer channel, moved here from the [`Call`].
611 done: Sender<TractResult<TVec<TValue>>>,
612}
613
614impl Call {
615 /// The inputs of each seat of this call: the batched ones sliced along
616 /// axis 0, the shared ones as they came. Every batched input says how many
617 /// seats the call asks for, so they must agree, and a call with none asks
618 /// for one.
619 fn explode(&self, batch_in: &[bool]) -> TractResult<Vec<TVec<TValue>>> {
620 ensure!(
621 self.inputs.len() == batch_in.len(),
622 "A call feeds {} inputs, the model takes {}",
623 self.inputs.len(),
624 batch_in.len()
625 );
626 let mut seats: Option<usize> = None;
627 for (ix, input) in self.inputs.iter().enumerate().filter(|(ix, _)| batch_in[*ix]) {
628 ensure!(
629 input.rank() > 0,
630 "Input {ix} carries the batch axis, so it can not be a scalar"
631 );
632 let asked = input.shape()[0];
633 ensure!(
634 seats.is_none_or(|seats| seats == asked),
635 "A call asks for as many seats as its batched inputs carry, and input {ix} carries \
636 {asked} against {} before it",
637 seats.unwrap_or(0)
638 );
639 seats = Some(asked);
640 }
641 let seats = seats.unwrap_or(1);
642 ensure!(seats > 0, "A call asks for one seat at least");
643 if seats == 1 {
644 return Ok(vec![self.inputs.clone()]);
645 }
646 (0..seats)
647 .map(|seat| {
648 self.inputs
649 .iter()
650 .zip(batch_in)
651 .map(|(input, is_batched)| {
652 if *is_batched {
653 Ok(input.slice(0, seat, seat + 1)?.into_tvalue())
654 } else {
655 Ok(input.clone())
656 }
657 })
658 .collect()
659 })
660 .collect()
661 }
662}
663
664impl Completer {
665 /// Whether every seat the call asked for has landed.
666 fn is_full(&self) -> bool {
667 self.served.iter().all(Option::is_some)
668 }
669
670 /// Answer the call with its seats' outputs: the batched ones stacked back
671 /// along axis 0 in the order it asked for its seats, the shared ones as the
672 /// first seat got them. Every seat must have landed.
673 fn answer(self, batch_out: &[bool]) {
674 let served: Vec<TVec<TValue>> =
675 self.served.into_iter().map(|outputs| outputs.unwrap()).collect();
676 let answer = if served.len() == 1 {
677 Ok(served.into_iter().next().unwrap())
678 } else {
679 let first = &served[0];
680 (0..first.len())
681 .map(|ix| {
682 if batch_out.get(ix).copied().unwrap_or(false) {
683 let seats: TVec<&Tensor> =
684 served.iter().map(|outputs| &*outputs[ix]).collect();
685 Ok(Tensor::stack_tensors(0, &seats)?.into_tvalue())
686 } else {
687 Ok(first[ix].clone())
688 }
689 })
690 .collect()
691 };
692 let _ = self.done.send(answer);
693 }
694}
695
696/// The seats the worker has to seat, and the calls they answer. A call explodes
697/// into seats as it arrives, so what a turn picks from is a queue of seats: it
698/// fills to the free lanes from the head, and a call wider than they are is
699/// split at the boundary rather than held until it fits whole.
700#[derive(Default)]
701struct Queue {
702 /// Seats waiting for a lane, oldest first: what a turn fills from.
703 seats: VecDeque<Seat>,
704 /// The calls with seats still to land, by call id.
705 completers: HashMap<u64, Completer>,
706 /// The id the next call gets, so seats of different calls never collide.
707 calls: u64,
708}
709
710impl Queue {
711 fn push(&mut self, call: Call, batch_in: &[bool]) {
712 let id = self.calls;
713 self.calls += 1;
714 match call.explode(batch_in) {
715 Ok(seats) => {
716 self.completers
717 .insert(id, Completer { served: vec![None; seats.len()], done: call.done });
718 for (ix, inputs) in seats.into_iter().enumerate() {
719 self.seats.push_back(Seat { call: id, lane: call.leased, ix, inputs });
720 }
721 }
722 Err(e) => {
723 let _ = call.done.send(Err(e));
724 }
725 }
726 }
727
728 /// Hand `seat` its turn's outputs, and answer the call when that was the
729 /// last seat it was waiting for. A failed seat fails the whole call at
730 /// once, and drops the seats of it still queued.
731 fn serve(&mut self, seat: Seat, outputs: TractResult<TVec<TValue>>, batch_out: &[bool]) {
732 if !self.completers.contains_key(&seat.call) {
733 return;
734 }
735 let outputs = match outputs {
736 Ok(outputs) => outputs,
737 Err(e) => {
738 let completer = self.completers.remove(&seat.call).unwrap();
739 self.seats.retain(|queued| queued.call != seat.call);
740 let _ = completer.done.send(Err(e));
741 return;
742 }
743 };
744 let completer = self.completers.get_mut(&seat.call).unwrap();
745 completer.served[seat.ix] = Some(outputs);
746 if completer.is_full() {
747 self.completers.remove(&seat.call).unwrap().answer(batch_out);
748 }
749 }
750}
751
752/// The worker thread's world: the one state it runs, the lanes it hands to
753/// streams, the seats it has still to run, and what it was set up with.
754///
755/// It is built on the calling thread and moved to the worker, which is the only
756/// thread to touch it afterwards -- `state` and `lanes` go together because
757/// taking a lane resets it, and a reset writes the state.
758struct Worker {
759 /// The one state every turn runs, spawned from the model as prepared.
760 state: Box<dyn State>,
761 /// Which lanes streams hold, and what a turn's seating is drawn from.
762 lanes: LaneTable,
763 /// Seats waiting for a lane, and the calls they answer.
764 queued: Queue,
765 /// One flag per input, true where axis 0 carries the batch symbol: those
766 /// are sliced per seat, the rest serve the whole turn.
767 batch_in: Vec<bool>,
768 /// The same per output: batched ones are sliced back per seat, shared ones
769 /// handed to every seat of the turn.
770 batch_out: Vec<bool>,
771 /// The widest turn to run, [`TRACT_MAX_SEATS`] clamped to the state's lanes.
772 max_seats: usize,
773 /// How long a turn waits for latecomers once its first seat is queued
774 /// ([`TRACT_TURN_LINGER_US`]), zero to run as soon as one is ready.
775 linger: Duration,
776 /// Turns and seats served, shared with the runnable the caller reads them
777 /// from.
778 counts: Arc<Counts>,
779}
780
781impl Worker {
782 /// Serve `queue` until it closes, which is what dropping the runnable does.
783 fn work(mut self, queue: Receiver<Request>) {
784 loop {
785 // The linger belongs to the turn a request opens, not to the
786 // request: taking or giving back a lane must not delay the turns
787 // behind it, and turns left waiting by a full one have lingered
788 // already.
789 while self.queued.seats.is_empty() {
790 match queue.recv() {
791 Ok(request) => self.serve(request),
792 Err(_) => return,
793 }
794 if !self.queued.seats.is_empty() && !self.linger.is_zero() {
795 thread::sleep(self.linger);
796 }
797 }
798 while let Ok(request) = queue.try_recv() {
799 self.serve(request);
800 }
801 let (seated, borrowed) = self.fill();
802 if seated.is_empty() {
803 continue;
804 }
805 self.counts.turns.fetch_add(1, Ordering::Relaxed);
806 self.counts.seats.fetch_add(seated.len() as u64, Ordering::Relaxed);
807 let served = self.run_turn(&seated);
808 for lane in borrowed {
809 let _ = self.lanes.give_back(lane);
810 }
811 match served {
812 Ok(per_seat) => {
813 for (seat, outputs) in seated.into_iter().zip(per_seat) {
814 self.queued.serve(seat, Ok(outputs), &self.batch_out);
815 }
816 }
817 Err(e) => {
818 let e = format!("{e:#}");
819 for seat in seated {
820 self.queued.serve(
821 seat,
822 Err(format_err!("Laned turn failed: {e}")),
823 &self.batch_out,
824 );
825 }
826 }
827 }
828 }
829 }
830
831 /// Hand out a lane or take it back there and then; queue the seats of a
832 /// call for the coming turns. Taking a lane resets it, which is why it
833 /// happens here rather than in the handle: it writes the state.
834 fn serve(&mut self, request: Request) {
835 match request {
836 Request::Spawn(taken) => {
837 let lane = match self.lanes.take() {
838 None => Err(format_err!(
839 "Every one of the {} lanes is taken",
840 self.lanes.max_lanes()
841 )),
842 Some(lane) => match self.state.reset_lanes(&[lane]) {
843 Ok(()) => Ok(lane),
844 Err(e) => {
845 let _ = self.lanes.give_back(lane);
846 Err(e)
847 }
848 },
849 };
850 let _ = taken.send(lane);
851 }
852 Request::Call(call) => self.queued.push(call, &self.batch_in),
853 Request::Drop(lane) => {
854 let _ = self.lanes.give_back(lane);
855 }
856 }
857 }
858
859 /// Seat the head of the queue, and the lanes borrowed to do it. A seat sits
860 /// in the lane its call leased, and a call's second and later seats of one
861 /// turn in lanes borrowed from whatever is free -- which resets them, so a
862 /// seat is always served by a lane holding nothing of another caller's.
863 fn fill(&mut self) -> (Vec<Seat>, Vec<LaneId>) {
864 let mut seated: Vec<Seat> = vec![];
865 let mut taken: Vec<LaneId> = vec![];
866 let mut borrowed: Vec<LaneId> = vec![];
867 let mut waiting: VecDeque<Seat> = VecDeque::new();
868 while let Some(mut seat) = self.queued.seats.pop_front() {
869 if seated.len() >= self.max_seats {
870 waiting.push_back(seat);
871 continue;
872 }
873 let lane = if taken.contains(&seat.lane) {
874 match self.lanes.take() {
875 Some(lane) => match self.state.reset_lanes(&[lane]) {
876 Ok(()) => {
877 borrowed.push(lane);
878 Some(lane)
879 }
880 Err(_) => {
881 let _ = self.lanes.give_back(lane);
882 None
883 }
884 },
885 None => None,
886 }
887 } else {
888 Some(seat.lane)
889 };
890 match lane {
891 Some(lane) => {
892 seat.lane = lane;
893 taken.push(lane);
894 seated.push(seat);
895 }
896 None => waiting.push_back(seat),
897 }
898 }
899 self.queued.seats = waiting;
900 (seated, borrowed)
901 }
902
903 /// Run `seated` as one turn: their batched inputs stacked along axis 0 in
904 /// seat order, their shared ones checked to agree, and the outputs sliced
905 /// back per seat.
906 fn run_turn(&mut self, seated: &[Seat]) -> TractResult<Vec<TVec<TValue>>> {
907 let seating = self.lanes.seat(seated.iter().map(|seat| seat.lane))?;
908 let mut batched: TVec<TValue> = tvec!();
909 for (ix, is_batched) in self.batch_in.iter().enumerate() {
910 if *is_batched {
911 let seats: TVec<&Tensor> = seated.iter().map(|seat| &*seat.inputs[ix]).collect();
912 for (seat, input) in seats.iter().enumerate() {
913 ensure!(
914 input.rank() > 0 && input.shape()[0] == 1,
915 "Seat {seat} feeds {:?} of input {ix}, which a turn seats one at a time",
916 input.shape()
917 );
918 }
919 batched.push(Tensor::stack_tensors(0, &seats)?.into_tvalue());
920 } else {
921 let shared = &seated[0].inputs[ix];
922 for (seat, turn) in seated.iter().enumerate().skip(1) {
923 ensure!(
924 turn.inputs[ix] == *shared,
925 "Input {ix} carries no batch axis, so one value of it serves the whole \
926 turn, and seats 0 and {seat} feed it different ones"
927 );
928 }
929 batched.push(shared.clone());
930 }
931 }
932 self.state.seat(seating)?;
933 let outputs = self.state.run(batched)?;
934 let mut per_seat: Vec<TVec<TValue>> = seated.iter().map(|_| tvec!()).collect();
935 for (ix, output) in outputs.into_iter().enumerate() {
936 if self.batch_out.get(ix).copied().unwrap_or(false) {
937 ensure!(
938 output.shape()[0] == seated.len(),
939 "The turn fills {} seats, output {ix} carries {:?}",
940 seated.len(),
941 output.shape()
942 );
943 for (seat, outputs) in per_seat.iter_mut().enumerate() {
944 outputs.push(output.slice(0, seat, seat + 1)?.into_tvalue());
945 }
946 } else {
947 for outputs in per_seat.iter_mut() {
948 outputs.push(output.clone());
949 }
950 }
951 }
952 Ok(per_seat)
953 }
954}
955
956// The suite is disabled on Wasm because a laned runnable spawns a thread.
957#[cfg(all(test, not(target_family = "wasm")))]
958mod laned_test {
959 use super::*;
960 use crate::ops::math::{add, mul};
961
962 /// `[BATCH, 3] * 2`, prepared on the cpu runtime: stateless, so its lanes
963 /// address nothing and only the seating of the batch axis is exercised.
964 fn doubler(max_lanes: usize) -> TractResult<LanedRunnable> {
965 let mut model = TypedModel::default();
966 let batch = model.symbols.sym("B");
967 let input = model.add_source("input", f32::fact(dims!(batch, 3)))?;
968 let two = model.add_const("two", tensor2(&[[2f32]]))?;
969 let doubled = model.wire_node("doubled", mul(), &[input, two])?;
970 model.select_output_outlets(&doubled)?;
971 let inner = DefaultRuntime.prepare(model)?;
972 LanedRunnable::wrap(inner.into(), max_lanes)
973 }
974
975 fn turn(handle: &mut Box<dyn State>, stream: usize, turn: usize) -> TractResult<()> {
976 let input = tensor2(&[[stream as f32, turn as f32, 1.]]);
977 let output = handle.run(tvec!(input.into_tvalue()))?;
978 assert_eq!(&*output[0], &tensor2(&[[2. * stream as f32, 2. * turn as f32, 2.]]));
979 Ok(())
980 }
981
982 /// `TRACT_TURN_LINGER_US` is process-wide, so the tests which widen the
983 /// turns hold this while they build their runnable and run their streams.
984 static LINGER: Mutex<()> = Mutex::new(());
985
986 /// A dropped handle hands its lane back through the queue, so the lane is
987 /// free at some point after the drop rather than at it.
988 fn spawn_once_free(runnable: &LanedRunnable) -> TractResult<Box<dyn State>> {
989 for _ in 0..100 {
990 if let Ok(handle) = runnable.spawn() {
991 return Ok(handle);
992 }
993 std::thread::sleep(Duration::from_millis(10));
994 }
995 runnable.spawn()
996 }
997
998 #[test]
999 fn one_stream_at_a_time() -> TractResult<()> {
1000 let runnable = doubler(2)?;
1001 let mut handle = runnable.spawn()?;
1002 for t in 0..4 {
1003 turn(&mut handle, 0, t)?;
1004 }
1005 Ok(())
1006 }
1007
1008 #[test]
1009 fn every_stream_gets_its_own_seat() -> TractResult<()> {
1010 let runnable = doubler(8)?;
1011 let streams: Vec<_> = (0..8)
1012 .map(|stream| {
1013 let runnable = runnable.clone();
1014 std::thread::spawn(move || -> TractResult<()> {
1015 let mut handle = runnable.spawn()?;
1016 for t in 0..32 {
1017 turn(&mut handle, stream, t)?;
1018 }
1019 Ok(())
1020 })
1021 })
1022 .collect();
1023 for stream in streams {
1024 stream.join().unwrap()?;
1025 }
1026 Ok(())
1027 }
1028
1029 #[test]
1030 fn a_turn_seats_the_streams_that_are_ready() -> TractResult<()> {
1031 let _linger = LINGER.lock().unwrap_or_else(|e| e.into_inner());
1032 TRACT_TURN_LINGER_US.set(20_000);
1033 let runnable = doubler(8);
1034 TRACT_TURN_LINGER_US.clear();
1035 let runnable = runnable?;
1036 let streams: Vec<_> = (0..8)
1037 .map(|stream| {
1038 let runnable = runnable.clone();
1039 std::thread::spawn(move || -> TractResult<()> {
1040 let mut handle = runnable.spawn()?;
1041 for t in 0..4 {
1042 turn(&mut handle, stream, t)?;
1043 }
1044 Ok(())
1045 })
1046 })
1047 .collect();
1048 for stream in streams {
1049 stream.join().unwrap()?;
1050 }
1051 let (turns, seats) = runnable.turns_and_seats();
1052 assert!(seats > turns, "{seats} seats over {turns} turns, none of them shared");
1053 Ok(())
1054 }
1055
1056 /// `[B, 3] * 2 + bias`, `bias` carrying no batch axis: the shape of a
1057 /// shared input, which one value of serves the whole turn.
1058 fn biased(max_lanes: usize) -> TractResult<LanedRunnable> {
1059 let mut model = TypedModel::default();
1060 let batch = model.symbols.sym("B");
1061 let input = model.add_source("input", f32::fact(dims!(batch, 3)))?;
1062 let bias = model.add_source("bias", f32::fact(dims!(1, 1)))?;
1063 let two = model.add_const("two", tensor2(&[[2f32]]))?;
1064 let doubled = model.wire_node("doubled", mul(), &[input, two])?;
1065 let biased = model.wire_node("biased", add(), &[doubled[0], bias])?;
1066 model.select_output_outlets(&biased)?;
1067 let inner = DefaultRuntime.prepare(model)?;
1068 LanedRunnable::wrap(inner.into(), max_lanes)
1069 }
1070
1071 /// One turn per stream, all of them at once, the `stream`th feeding
1072 /// `biases[stream]`. Every lane is taken before any turn is queued, so the
1073 /// linger has the turns to seat together rather than a `spawn` to serve.
1074 fn biased_turns(runnable: &LanedRunnable, biases: &[f32]) -> TractResult<Vec<TractResult<()>>> {
1075 let handles: Vec<Box<dyn State>> =
1076 biases.iter().map(|_| runnable.spawn()).collect::<TractResult<_>>()?;
1077 let streams: Vec<_> = handles
1078 .into_iter()
1079 .zip(biases.iter().copied())
1080 .map(|(mut handle, bias)| {
1081 std::thread::spawn(move || -> TractResult<()> {
1082 handle.run(tvec!(
1083 tensor2(&[[1f32, 2., 3.]]).into_tvalue(),
1084 tensor2(&[[bias]]).into_tvalue()
1085 ))?;
1086 Ok(())
1087 })
1088 })
1089 .collect();
1090 Ok(streams.into_iter().map(|stream| stream.join().unwrap()).collect())
1091 }
1092
1093 #[test]
1094 fn seats_agreeing_on_a_shared_input_share_a_turn() -> TractResult<()> {
1095 let _linger = LINGER.lock().unwrap_or_else(|e| e.into_inner());
1096 TRACT_TURN_LINGER_US.set(100_000);
1097 let runnable = biased(2);
1098 TRACT_TURN_LINGER_US.clear();
1099 let runnable = runnable?;
1100 let served = biased_turns(&runnable, &[7., 7.])?;
1101 assert!(served.iter().all(|s| s.is_ok()), "{served:?}");
1102 assert_eq!(runnable.turns_and_seats(), (1, 2));
1103 Ok(())
1104 }
1105
1106 #[test]
1107 fn seats_disagreeing_on_a_shared_input_fail_the_turn() -> TractResult<()> {
1108 let _linger = LINGER.lock().unwrap_or_else(|e| e.into_inner());
1109 TRACT_TURN_LINGER_US.set(100_000);
1110 let runnable = biased(2);
1111 TRACT_TURN_LINGER_US.clear();
1112 let runnable = runnable?;
1113 let served = biased_turns(&runnable, &[7., 8.])?;
1114 assert_eq!(runnable.turns_and_seats(), (1, 2));
1115 for stream in &served {
1116 let error = format!("{:#}", stream.as_ref().unwrap_err());
1117 assert!(error.contains("seats 0 and 1 feed it different ones"), "{error}");
1118 }
1119 Ok(())
1120 }
1121
1122 /// A call feeding `seats` seats of the doubler at once: seat `s` feeds
1123 /// `[s, s + 1, s + 2]`, and the call is answered in that order.
1124 fn wide_call(handle: &mut Box<dyn State>, seats: usize) -> TractResult<()> {
1125 let fed: Vec<f32> =
1126 (0..seats).flat_map(|s| [s as f32, s as f32 + 1., s as f32 + 2.]).collect();
1127 let doubled: Vec<f32> = fed.iter().map(|x| 2. * x).collect();
1128 let output = handle.run(tvec!(Tensor::from_shape(&[seats, 3], &fed)?.into_tvalue()))?;
1129 assert_eq!(&*output[0], &Tensor::from_shape(&[seats, 3], &doubled)?);
1130 Ok(())
1131 }
1132
1133 #[test]
1134 fn a_call_asks_for_as_many_seats_as_it_feeds() -> TractResult<()> {
1135 let runnable = doubler(4)?;
1136 let mut handle = runnable.spawn()?;
1137 wide_call(&mut handle, 3)?;
1138 assert_eq!(runnable.turns_and_seats(), (1, 3));
1139 Ok(())
1140 }
1141
1142 #[test]
1143 fn a_call_wider_than_the_lanes_is_split_across_turns() -> TractResult<()> {
1144 let runnable = doubler(2)?;
1145 let mut handle = runnable.spawn()?;
1146 wide_call(&mut handle, 5)?;
1147 let (turns, seats) = runnable.turns_and_seats();
1148 assert_eq!(seats, 5);
1149 assert_eq!(turns, 3, "5 seats over 2 lanes are 3 turns, got {turns}");
1150 Ok(())
1151 }
1152
1153 #[test]
1154 fn a_seat_borrows_a_free_lane_only() -> TractResult<()> {
1155 let runnable = doubler(2)?;
1156 let mut handle = runnable.spawn()?;
1157 let mut other = runnable.spawn()?;
1158 turn(&mut other, 1, 0)?;
1159 wide_call(&mut handle, 4)?;
1160 let (turns, seats) = runnable.turns_and_seats();
1161 assert_eq!(seats, 5);
1162 assert_eq!(turns, 5, "another stream holds the second lane, so each seat is a turn");
1163 Ok(())
1164 }
1165
1166 /// `[B, 3] + [B, 3]`: two batched inputs, which a call has to feed the same
1167 /// number of seats of.
1168 fn adder(max_lanes: usize) -> TractResult<LanedRunnable> {
1169 let mut model = TypedModel::default();
1170 let batch = model.symbols.sym("B");
1171 let left = model.add_source("left", f32::fact(dims!(batch, 3)))?;
1172 let right = model.add_source("right", f32::fact(dims!(batch, 3)))?;
1173 let sum = model.wire_node("sum", add(), &[left, right])?;
1174 model.select_output_outlets(&sum)?;
1175 let inner = DefaultRuntime.prepare(model)?;
1176 LanedRunnable::wrap(inner.into(), max_lanes)
1177 }
1178
1179 #[test]
1180 fn a_call_whose_batched_inputs_disagree_on_seats_fails() -> TractResult<()> {
1181 let runnable = adder(4)?;
1182 let mut handle = runnable.spawn()?;
1183 let error = handle
1184 .run(tvec!(
1185 tensor2(&[[1f32, 2., 3.], [4., 5., 6.]]).into_tvalue(),
1186 tensor2(&[[7f32, 8., 9.]]).into_tvalue()
1187 ))
1188 .unwrap_err();
1189 let error = format!("{error:#}");
1190 assert!(error.contains("input 1 carries 1 against 2"), "{error}");
1191 Ok(())
1192 }
1193
1194 #[test]
1195 fn a_batch_off_axis_zero_says_which_axis_it_sits_on() -> TractResult<()> {
1196 let mut model = TypedModel::default();
1197 let batch = model.symbols.sym("B");
1198 let input = model.add_source("input", f32::fact(dims!(3, batch)))?;
1199 let two = model.add_const("two", tensor2(&[[2f32]]))?;
1200 let doubled = model.wire_node("doubled", mul(), &[input, two])?;
1201 model.select_output_outlets(&doubled)?;
1202 let inner = DefaultRuntime.prepare(model)?;
1203 let error = format!("{:#}", LanedRunnable::wrap(inner.into(), 2).unwrap_err());
1204 assert!(error.contains("input 0 carries B on axis 1"), "{error}");
1205 assert!(error.contains("Batchify"), "{error}");
1206 Ok(())
1207 }
1208
1209 #[test]
1210 fn two_batch_symbols_are_named() -> TractResult<()> {
1211 let mut model = TypedModel::default();
1212 let left_batch = model.symbols.sym("L");
1213 let right_batch = model.symbols.sym("R");
1214 let left = model.add_source("left", f32::fact(dims!(left_batch, 3)))?;
1215 let right = model.add_source("right", f32::fact(dims!(right_batch, 3)))?;
1216 let sum = model.wire_node("sum", add(), &[left, right])?;
1217 model.select_output_outlets(&sum)?;
1218 let inner = DefaultRuntime.prepare(model)?;
1219 let error = format!("{:#}", LanedRunnable::wrap(inner.into(), 2).unwrap_err());
1220 assert!(error.contains("L and R"), "{error}");
1221 Ok(())
1222 }
1223
1224 #[test]
1225 fn a_dropped_stream_gives_its_lane_back() -> TractResult<()> {
1226 let runnable = doubler(1)?;
1227 let mut handle = runnable.spawn()?;
1228 turn(&mut handle, 0, 0)?;
1229 assert!(runnable.spawn().is_err());
1230 let clone = dyn_clone::clone_box(&*handle);
1231 drop(handle);
1232 assert!(runnable.spawn().is_err());
1233 drop(clone);
1234 let mut handle = spawn_once_free(&runnable)?;
1235 turn(&mut handle, 1, 0)?;
1236 Ok(())
1237 }
1238}
1239
1240#[cfg(test)]
1241mod lane_table_test {
1242 use super::*;
1243
1244 #[test]
1245 fn takes_the_lowest_free_lane() -> TractResult<()> {
1246 let mut table = LaneTable::new(3)?;
1247 assert_eq!(table.take(), Some(LaneId(0)));
1248 assert_eq!(table.take(), Some(LaneId(1)));
1249 table.give_back(LaneId(0))?;
1250 assert_eq!(table.take(), Some(LaneId(0)));
1251 assert_eq!(table.taken(), 2);
1252 Ok(())
1253 }
1254
1255 #[test]
1256 fn runs_out_of_lanes() -> TractResult<()> {
1257 let mut table = LaneTable::new(1)?;
1258 assert_eq!(table.take(), Some(LaneId(0)));
1259 assert_eq!(table.take(), None);
1260 Ok(())
1261 }
1262
1263 #[test]
1264 fn gives_back_a_taken_lane_only() -> TractResult<()> {
1265 let mut table = LaneTable::new(2)?;
1266 assert!(table.give_back(LaneId(0)).is_err());
1267 table.take();
1268 table.give_back(LaneId(0))?;
1269 assert!(table.give_back(LaneId(0)).is_err());
1270 assert!(table.give_back(LaneId(7)).is_err());
1271 Ok(())
1272 }
1273
1274 #[test]
1275 fn seats_taken_lanes_in_order() -> TractResult<()> {
1276 let mut table = LaneTable::new(4)?;
1277 table.take();
1278 table.take();
1279 table.take();
1280 table.give_back(LaneId(1))?;
1281 let seating = table.seat([LaneId(2), LaneId(0)])?;
1282 assert_eq!(seating.max_lanes(), 4);
1283 assert_eq!(seating.occupancy(), 2);
1284 assert_eq!(seating.address(0), (Some(0), Some(2)));
1285 assert_eq!(seating.address(1), (Some(1), Some(0)));
1286 assert!(table.seat([LaneId(0), LaneId(1)]).is_err());
1287 assert!(table.seat([LaneId(0), LaneId(0)]).is_err());
1288 Ok(())
1289 }
1290}