1use 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#[derive(Debug, Clone)]
15pub struct RealtimeTrace {
16 batch: usize,
17 generated_audio_codebooks: usize,
18 frames: Vec<RealtimeOutputFrame>,
19}
20
21impl RealtimeTrace {
22 pub const fn batch(&self) -> usize {
24 self.batch
25 }
26
27 pub const fn generated_audio_codebooks(&self) -> usize {
29 self.generated_audio_codebooks
30 }
31
32 pub fn frames(&self) -> &[RealtimeOutputFrame] {
34 &self.frames
35 }
36
37 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 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 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
147pub 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
176pub 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#[derive(Debug, thiserror::Error)]
257pub enum RealtimeTraceError {
258 #[error(transparent)]
260 Evidence(#[from] EvidenceError),
261 #[error("encoded realtime input must have shape [batch, codebooks, frames], got {0:?}")]
263 InputShape(Vec<usize>),
264 #[error("encoded realtime token {0} does not fit I32")]
266 TokenRange(i64),
267 #[error("realtime frame index {0} does not fit I64")]
269 IndexOverflow(usize),
270 #[error("realtime trace batch changed from {expected} to {actual}")]
272 BatchChanged {
273 expected: usize,
275 actual: usize,
277 },
278 #[error("realtime frame has {values} values for batch {batch} and width {width}")]
280 FrameWidth {
281 batch: usize,
283 width: usize,
285 values: usize,
287 },
288 #[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}