Skip to main content

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}