Skip to main content

eredu_evaluation/
realtime.rs

1//! Backend-neutral execution and observation of realtime traces.
2
3use eredu_core::{
4    scheduler::{RequestId, SchedulerLimits},
5    ObservationSet, ObservationValue, RealtimeBackend, RealtimeInputFrame, RealtimeModel,
6    RealtimeOutputFrame, RealtimeSampling, RealtimeScheduler, TensorObservation,
7    TensorObservationData,
8};
9use eredu_nn::Tensor;
10
11use crate::{observe_i32_tensor, EvidenceError};
12
13/// Completed portable outputs from one realtime request.
14#[derive(Debug, Clone)]
15pub struct RealtimeTrace {
16    batch: usize,
17    generated_audio_codebooks: usize,
18    frames: Vec<RealtimeOutputFrame>,
19}
20
21impl RealtimeTrace {
22    /// Stable batch dimension.
23    pub const fn batch(&self) -> usize {
24        self.batch
25    }
26
27    /// Generated audio codebooks per output frame.
28    pub const fn generated_audio_codebooks(&self) -> usize {
29        self.generated_audio_codebooks
30    }
31
32    /// Completed frames in submission order.
33    pub fn frames(&self) -> &[RealtimeOutputFrame] {
34        &self.frames
35    }
36
37    /// Converts common token streams and per-decision diagnostics to evidence.
38    pub fn observations(&self) -> Result<ObservationSet, RealtimeTraceError> {
39        let mut observations = ObservationSet::new();
40        observations.insert(
41            "trace.text_tokens",
42            ObservationValue::Tensor(integer_tensor(
43                vec![self.batch, self.frames.len()],
44                transpose_frame_values(
45                    self.frames.iter().map(RealtimeOutputFrame::text_tokens),
46                    self.batch,
47                    1,
48                )?,
49            )?),
50        )?;
51        observations.insert(
52            "trace.sampled_audio_tokens",
53            ObservationValue::Tensor(integer_tensor(
54                vec![
55                    self.batch,
56                    self.generated_audio_codebooks,
57                    self.frames.len(),
58                ],
59                transpose_frame_values(
60                    self.frames
61                        .iter()
62                        .map(RealtimeOutputFrame::sampled_audio_tokens),
63                    self.batch,
64                    self.generated_audio_codebooks,
65                )?,
66            )?),
67        )?;
68        let emitted = self
69            .frames
70            .iter()
71            .filter_map(RealtimeOutputFrame::output_audio_tokens)
72            .collect::<Vec<_>>();
73        observations.insert(
74            "trace.output_audio_tokens",
75            ObservationValue::Tensor(integer_tensor(
76                vec![self.batch, self.generated_audio_codebooks, emitted.len()],
77                transpose_frame_values(
78                    emitted.iter().copied(),
79                    self.batch,
80                    self.generated_audio_codebooks,
81                )?,
82            )?),
83        )?;
84        for (frame, output) in self.frames.iter().enumerate() {
85            for diagnostic in output.diagnostics() {
86                observations.insert(
87                    format!(
88                        "frames.{frame}.decisions.{}.logits",
89                        diagnostic.prediction()
90                    ),
91                    ObservationValue::Tensor(diagnostic.tensor().clone()),
92                )?;
93            }
94        }
95        Ok(observations)
96    }
97
98    /// Stacks text followed by sampled-audio tokens as `[batch, width, frames]`.
99    pub fn combined_sampled_tokens(
100        &self,
101        skip_frames: usize,
102    ) -> Result<TensorObservation, RealtimeTraceError> {
103        let frames = self.frames.get(skip_frames..).unwrap_or_default();
104        let width = self.generated_audio_codebooks + 1;
105        let mut values = Vec::with_capacity(self.batch * width * frames.len());
106        for batch_index in 0..self.batch {
107            for value_index in 0..width {
108                for frame in frames {
109                    let value = if value_index == 0 {
110                        *frame.text_tokens().get(batch_index).ok_or(
111                            RealtimeTraceError::FrameWidth {
112                                batch: self.batch,
113                                width: 1,
114                                values: frame.text_tokens().len(),
115                            },
116                        )?
117                    } else {
118                        *frame
119                            .sampled_audio_tokens()
120                            .get(batch_index * self.generated_audio_codebooks + value_index - 1)
121                            .ok_or(RealtimeTraceError::FrameWidth {
122                                batch: self.batch,
123                                width: self.generated_audio_codebooks,
124                                values: frame.sampled_audio_tokens().len(),
125                            })?
126                    };
127                    values.push(i64::from(value));
128                }
129            }
130        }
131        integer_tensor(vec![self.batch, width, frames.len()], values)
132    }
133
134    /// Frame indices at which delay-aligned output audio was emitted.
135    pub fn emitted_frame_indices(&self) -> Result<TensorObservation, RealtimeTraceError> {
136        let values = self
137            .frames
138            .iter()
139            .enumerate()
140            .filter_map(|(index, frame)| frame.output_audio_tokens().is_some().then_some(index))
141            .map(|index| i64::try_from(index).map_err(|_| RealtimeTraceError::IndexOverflow(index)))
142            .collect::<Result<Vec<_>, _>>()?;
143        integer_tensor(vec![values.len()], values)
144    }
145}
146
147/// Converts neutral `[batch, codebooks, frames]` integer tokens to frame inputs.
148pub fn encoded_audio_frames<T: Tensor>(
149    tokens: &T,
150    context: &T::Context,
151) -> Result<Vec<RealtimeInputFrame>, RealtimeTraceError> {
152    let observed = observe_i32_tensor(tokens, context)?;
153    let [batch, codebooks, frames] = observed.shape() else {
154        return Err(RealtimeTraceError::InputShape(observed.shape().to_vec()));
155    };
156    let TensorObservationData::I64(values) = observed.data() else {
157        unreachable!("observe_i32_tensor always produces I64 host values")
158    };
159    (0..*frames)
160        .map(|frame| {
161            let mut frame_tokens = Vec::with_capacity(batch * codebooks);
162            for batch_index in 0..*batch {
163                for codebook in 0..*codebooks {
164                    let index = (batch_index * codebooks + codebook) * frames + frame;
165                    frame_tokens.push(
166                        i32::try_from(values[index])
167                            .map_err(|_| RealtimeTraceError::TokenRange(values[index]))?,
168                    );
169                }
170            }
171            Ok(RealtimeInputFrame::new(*batch, frame_tokens))
172        })
173        .collect()
174}
175
176/// Executes portable encoded frames through any realtime backend.
177pub fn run_realtime_trace<B>(
178    model: &mut RealtimeModel<B>,
179    inputs: impl IntoIterator<Item = RealtimeInputFrame>,
180    sampling: RealtimeSampling,
181) -> Result<RealtimeTrace, Box<dyn std::error::Error + Send + Sync>>
182where
183    B: RealtimeBackend,
184{
185    let request = RequestId::new(0);
186    let mut scheduler = RealtimeScheduler::new(model, SchedulerLimits::new(1, 1)?)?;
187    scheduler.register_request(model, request, sampling)?;
188    let mut batch = None;
189    let mut frames = Vec::new();
190    for frame in inputs {
191        match batch {
192            Some(expected) if expected != frame.batch() => {
193                return Err(Box::new(RealtimeTraceError::BatchChanged {
194                    expected,
195                    actual: frame.batch(),
196                }));
197            }
198            None => batch = Some(frame.batch()),
199            _ => {}
200        }
201        let input = model.backend().materialize_input(model.model(), &frame)?;
202        scheduler.enqueue(model, request, input)?;
203        let output = loop {
204            if let Some(completed) = scheduler.run_queued(model)?.pop() {
205                break model.backend().observe_output(completed.output())?;
206            }
207            std::thread::yield_now();
208        };
209        frames.push(output);
210    }
211    scheduler.finish_request(request)?;
212    Ok(RealtimeTrace {
213        batch: batch.unwrap_or(1),
214        generated_audio_codebooks: model.speech_config().generated_audio_codebooks(),
215        frames,
216    })
217}
218
219fn transpose_frame_values<'a>(
220    frames: impl IntoIterator<Item = &'a [i32]>,
221    batch: usize,
222    width: usize,
223) -> Result<Vec<i64>, RealtimeTraceError> {
224    let frames = frames.into_iter().collect::<Vec<_>>();
225    for values in &frames {
226        if values.len() != batch.saturating_mul(width) {
227            return Err(RealtimeTraceError::FrameWidth {
228                batch,
229                width,
230                values: values.len(),
231            });
232        }
233    }
234    let mut output = Vec::with_capacity(batch * width * frames.len());
235    for batch_index in 0..batch {
236        for value_index in 0..width {
237            for frame in &frames {
238                output.push(i64::from(frame[batch_index * width + value_index]));
239            }
240        }
241    }
242    Ok(output)
243}
244
245fn integer_tensor(
246    shape: Vec<usize>,
247    values: Vec<i64>,
248) -> Result<TensorObservation, RealtimeTraceError> {
249    Ok(TensorObservation::new(
250        shape,
251        TensorObservationData::I64(values),
252    )?)
253}
254
255/// Invalid portable realtime trace evidence.
256#[derive(Debug, thiserror::Error)]
257pub enum RealtimeTraceError {
258    /// Tensor host observation failed.
259    #[error(transparent)]
260    Evidence(#[from] EvidenceError),
261    /// Encoded input must be `[batch, codebooks, frames]`.
262    #[error("encoded realtime input must have shape [batch, codebooks, frames], got {0:?}")]
263    InputShape(Vec<usize>),
264    /// One portable token does not fit the realtime I32 domain.
265    #[error("encoded realtime token {0} does not fit I32")]
266    TokenRange(i64),
267    /// A frame ordinal cannot be represented in evidence.
268    #[error("realtime frame index {0} does not fit I64")]
269    IndexOverflow(usize),
270    /// Input batch changed within one request.
271    #[error("realtime trace batch changed from {expected} to {actual}")]
272    BatchChanged {
273        /// Initial batch.
274        expected: usize,
275        /// Later batch.
276        actual: usize,
277    },
278    /// A completed frame has incompatible token geometry.
279    #[error("realtime frame has {values} values for batch {batch} and width {width}")]
280    FrameWidth {
281        /// Trace batch.
282        batch: usize,
283        /// Expected values per batch row.
284        width: usize,
285        /// Observed values.
286        values: usize,
287    },
288    /// Portable observation construction failed.
289    #[error(transparent)]
290    Observation(#[from] eredu_core::ObservationError),
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296
297    #[test]
298    fn trace_observations_transpose_frame_major_tokens() {
299        let trace = RealtimeTrace {
300            batch: 1,
301            generated_audio_codebooks: 2,
302            frames: vec![
303                RealtimeOutputFrame::new(1, vec![1], vec![2, 3], vec![2, 3], None, Vec::new()),
304                RealtimeOutputFrame::new(
305                    1,
306                    vec![4],
307                    vec![5, 6],
308                    vec![5, 6],
309                    Some(vec![7, 8]),
310                    Vec::new(),
311                ),
312            ],
313        };
314        let observations = trace.observations().unwrap();
315        let Some(ObservationValue::Tensor(text)) = observations.get("trace.text_tokens") else {
316            panic!("text trace must be a tensor");
317        };
318        assert_eq!(text.shape(), [1, 2]);
319        assert_eq!(text.data(), &TensorObservationData::I64(vec![1, 4]));
320    }
321}