1use crate::internal::*;
2use tract_linalg::MmmDispatch;
3use tract_linalg::mmm::{AsInputValue, FusedSpec, MMMInputValue, MatMatMul, Query};
4use tract_linalg::pack::PackedFormat;
5use tract_linalg::routines::Func;
6use tract_ndarray::prelude::*;
7
8type PackedR = (Box<dyn MatMatMul>, usize, Box<dyn MMMInputValue>);
12
13#[derive(Debug, Clone, Hash, PartialEq, Eq)]
30pub struct GruSeq {
31 pub hidden: usize,
32 pub has_bias: bool,
33 pub chunk: isize,
35 pub emit_y: bool,
38 pub reset_every_turn: bool,
41}
42
43#[derive(Default)]
44struct GruSeqState {
45 h: Option<Tensor>,
46 packed_r: Option<PackedR>,
50}
51
52impl std::fmt::Debug for GruSeqState {
53 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
54 f.debug_struct("GruSeqState").field("h", &self.h).finish()
55 }
56}
57
58impl Clone for GruSeqState {
59 fn clone(&self) -> Self {
60 GruSeqState { h: self.h.clone(), packed_r: None }
62 }
63}
64
65impl Op for GruSeq {
66 fn name(&self) -> StaticName {
67 "GruSeq".into()
68 }
69 fn info(&self) -> TractResult<Vec<String>> {
70 Ok(vec![format!(
71 "hidden={} bias={} chunk={} reset_every_turn={} emit_y={}",
72 self.hidden, self.has_bias, self.chunk, self.reset_every_turn, self.emit_y
73 )])
74 }
75 op_as_typed_op!();
76}
77
78impl EvalOp for GruSeq {
79 not_out_of_plan!();
80
81 fn state(&self, _ctx: &EvalContext) -> TractResult<Option<Box<dyn OpState>>> {
84 Ok(Some(Box::<GruSeqState>::default()))
85 }
86}
87
88impl OpState for GruSeqState {
89 fn eval(
90 &mut self,
91 _ctx: &EvalContext,
92 op: &dyn Op,
93 inputs: TVec<TValue>,
94 ) -> TractResult<TVec<TValue>> {
95 let op = op.downcast_ref::<GruSeq>().context("wrong op")?;
96 op.eval_with(&mut self.h, &mut self.packed_r, inputs)
97 }
98
99 fn reset_lanes(&mut self, _lanes: &[LaneId]) -> TractResult<()> {
102 bail!("GruSeq is not lane-aware: it carries a single hidden state")
103 }
104}
105
106impl GruSeq {
107 fn eval_with(
108 &self,
109 carry: &mut Option<Tensor>,
110 packed_r: &mut Option<PackedR>,
111 inputs: TVec<TValue>,
112 ) -> TractResult<TVec<TValue>> {
113 let (x, w, r) = (&inputs[0], &inputs[1], &inputs[2]);
114 let b = if self.has_bias { Some(&inputs[3]) } else { None };
115 let h0 = &inputs[inputs.len() - 1];
116 let h = self.hidden;
117
118 let x = x.to_plain_array_view::<f32>()?.into_dimensionality::<Ix3>()?; let w = w.to_plain_array_view::<f32>()?.into_dimensionality::<Ix2>()?; let (batch, t_len, in_size) = x.dim();
121
122 let (wb, rb) = match b {
123 Some(b) => {
124 let v = b.to_plain_array_view::<f32>()?;
126 let b = match v.ndim() {
127 2 => v.into_dimensionality::<Ix2>()?.index_axis_move(Axis(0), 0).to_owned(),
128 _ => v.into_dimensionality::<Ix1>()?.to_owned(),
129 };
130 (Some(b.slice(s![0..3 * h]).to_owned()), Some(b.slice(s![3 * h..6 * h]).to_owned()))
131 }
132 None => (None, None),
133 };
134
135 let x_permuted = x.permuted_axes([1, 0, 2]);
139 let x_by_step = x_permuted.as_standard_layout();
140 let mut xw = x_by_step.to_shape((t_len * batch, in_size))?.dot(&w.t());
141 if let Some(wb) = &wb {
142 xw += &wb.view().insert_axis(Axis(0));
143 }
144
145 if packed_r.is_none() {
149 let query = Query {
155 allow_extractor: false,
156 ..Query::plain(f32::datum_type(), Some(3 * h), Some(h), Some(batch))
157 };
158 let (mmm, packing, _) = MmmDispatch::native()
159 .pick(&query)
160 .context("no matmul kernel for the recurrent product")?;
161 let (pack_a, _) = &mmm.packings()[packing];
162 let r_t = r.clone().into_tensor();
163 let pa = pack_a.prepare_one(&r_t, 1, 0)?;
164 *packed_r = Some((mmm, packing, pa));
165 }
166 let (mmm, packing, pa) = packed_r.as_ref().unwrap();
167 let (_, pack_b) = &mmm.packings()[*packing];
168
169 let mut ht: Tensor = match carry.as_ref().filter(|_| !self.reset_every_turn) {
172 Some(c) => squeeze_state(c, batch, h)?,
173 None => squeeze_state(h0, batch, h)?,
174 };
175
176 let sigmoid = Func::Sigmoid.ew_f32()?;
177 let tanh = Func::Tanh.ew_f32()?;
178
179 let pf = pack_b
184 .downcast_ref::<PackedFormat>()
185 .context("recurrent product expects a plainly packed B side")?;
186 let mut packed_ht = pf.new_packed_buffer(h, batch)?;
187 let mut rh = Tensor::zero::<f32>(&[batch, 3 * h])?;
190 let mut h_next = Tensor::zero::<f32>(&[batch, h])?;
191 let mut y = Array3::<f32>::zeros((batch, if self.emit_y { t_len } else { 0 }, h));
192
193 for step in 0..t_len {
194 let t = if self.chunk < 0 { t_len - 1 - step } else { step };
195
196 {
198 pf.repack_tensor_view(&mut packed_ht, &ht.view(), 1, 0)?;
202 let pb = &packed_ht;
203 unsafe {
204 let c = mmm.c_view(Some(1), Some(0)).wrap(&rh.view_mut());
205 mmm.run(
206 3 * h,
207 batch,
208 &[
209 FusedSpec::AddMatMul {
210 a: AsInputValue::Borrowed(&**pa),
211 b: AsInputValue::Borrowed(pb),
212 packing: 0,
213 },
214 FusedSpec::Store(c),
215 ],
216 )?;
217 }
218 if let Some(rb) = &rb {
219 let rb = rb.as_slice().context("R-side bias not contiguous")?;
220 for row in rh.try_as_plain_ram_mut()?.as_slice_mut::<f32>()?.chunks_mut(3 * h) {
221 for (o, b) in row.iter_mut().zip(rb) {
222 *o += b;
223 }
224 }
225 }
226 }
227
228 let xh_row = &mut xw.as_slice_mut().context("xw not contiguous")?
229 [t * batch * 3 * h..(t + 1) * batch * 3 * h];
230 crate::ops::gru_cell::gru_cell_rows(
231 h,
232 batch,
233 xh_row,
234 rh.try_as_plain_ram()?.as_slice::<f32>()?,
235 ht.try_as_plain_ram()?.as_slice::<f32>()?,
236 h_next.try_as_plain_ram_mut()?.as_slice_mut::<f32>()?,
237 &*sigmoid,
238 &*tanh,
239 )?;
240 std::mem::swap(&mut ht, &mut h_next);
241 if self.emit_y {
242 y.slice_mut(s![.., t, ..])
243 .assign(&ht.to_plain_array_view::<f32>()?.into_dimensionality::<Ix2>()?);
244 }
245 }
246
247 *carry = if self.reset_every_turn { None } else { Some(ht.clone()) };
248 let mut h_out = ht;
249 h_out.insert_axis(1)?; Ok(tvec!(y.into_tensor().into(), h_out.into()))
251 }
252}
253
254fn squeeze_state(t: &Tensor, batch: usize, h: usize) -> TractResult<Tensor> {
256 let mut t = t.clone().into_tensor();
257 ensure!(
258 t.len() == batch * h,
259 "GruSeq state holds {} elements, expected batch {batch} x hidden {h}",
260 t.len()
261 );
262 t.set_shape(&[batch, h])?;
263 Ok(t)
264}
265
266impl TypedOp for GruSeq {
267 fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
268 let x = inputs[0];
269 let batch = x.shape[0].clone();
270 let t = x.shape[1].clone();
271 let y_len = if self.emit_y { t } else { 0.to_dim() };
272 Ok(tvec!(
273 f32::fact([batch.clone(), y_len, self.hidden.to_dim()]),
274 f32::fact([batch, 1.to_dim(), self.hidden.to_dim()])
275 ))
276 }
277 as_op!();
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283 use crate::ops::gru_cell::GruEpilogue;
284
285 fn reference(
290 x: &Array3<f32>,
291 w: &Array2<f32>,
292 r: &Array2<f32>,
293 b: Option<&Array1<f32>>,
294 h0: &Array2<f32>,
295 hidden: usize,
296 backward: bool,
297 ) -> (Array3<f32>, Array2<f32>) {
298 let (batch, t_len, _) = x.dim();
299 let mut ht = h0.clone();
300 let mut y = Array3::<f32>::zeros((batch, t_len, hidden));
301 for step in 0..t_len {
302 let t = if backward { t_len - 1 - step } else { step };
303 let mut xh = x.slice(s![.., t, ..]).to_owned().dot(&w.t());
304 let mut rh = ht.dot(&r.t());
305 if let Some(b) = b {
306 xh += &b.slice(s![0..3 * hidden]).insert_axis(Axis(0));
307 rh += &b.slice(s![3 * hidden..6 * hidden]).insert_axis(Axis(0));
308 }
309 let out = GruEpilogue { hidden }
310 .eval(
311 &EvalContext::out_of_plan(),
312 tvec!(
313 xh.into_tensor().into(),
314 rh.into_tensor().into(),
315 ht.clone().into_tensor().into()
316 ),
317 )
318 .unwrap();
319 ht = out[0]
320 .to_plain_array_view::<f32>()
321 .unwrap()
322 .into_dimensionality::<Ix2>()
323 .unwrap()
324 .to_owned();
325 y.slice_mut(s![.., t, ..]).assign(&ht);
326 }
327 (y, ht)
328 }
329
330 fn run_case(batch: usize, t_len: usize, backward: bool, bias: bool) {
331 let (input, hidden) = (12usize, 16usize);
332 let f = |n: usize, k: f32| Array1::from_iter((0..n).map(|i| ((i as f32) * k).sin() * 0.3));
333 let x = f(batch * t_len * input, 0.7).into_shape_with_order((batch, t_len, input)).unwrap();
334 let w = f(3 * hidden * input, 0.31).into_shape_with_order((3 * hidden, input)).unwrap();
335 let r = f(3 * hidden * hidden, 0.17).into_shape_with_order((3 * hidden, hidden)).unwrap();
336 let b = bias.then(|| f(6 * hidden, 0.11));
337 let h0 = Array2::<f32>::zeros((batch, hidden));
338
339 let (want_y, want_h) = reference(&x, &w, &r, b.as_ref(), &h0, hidden, backward);
340
341 let op = GruSeq {
342 hidden,
343 has_bias: bias,
344 chunk: if backward { -1 } else { 1 },
345 reset_every_turn: false,
346 emit_y: true,
347 };
348 let mut inputs: TVec<TValue> = tvec!(
349 x.clone().into_tensor().into(),
350 w.clone().into_tensor().into(),
351 r.clone().into_tensor().into()
352 );
353 if let Some(b) = &b {
354 inputs.push(b.clone().into_tensor().into());
355 }
356 inputs.push(h0.clone().into_tensor().into());
357 let mut carry = None;
358 let mut packed = None;
359 let got = op.eval_with(&mut carry, &mut packed, inputs).unwrap();
360
361 let got_y = got[0].clone().into_tensor();
362 let got_h = got[1]
363 .to_plain_array_view::<f32>()
364 .unwrap()
365 .into_dimensionality::<Ix3>()
366 .unwrap()
367 .index_axis_move(Axis(1), 0)
368 .to_owned();
369
370 got_y.close_enough(&want_y.into_tensor(), Approximation::Approximate).unwrap_or_else(|e| {
376 panic!("Y mismatch b={batch} t={t_len} backward={backward} bias={bias}: {e}")
377 });
378 got_h
379 .into_tensor()
380 .close_enough(&want_h.into_tensor(), Approximation::Approximate)
381 .unwrap_or_else(|e| {
382 panic!("Y_h mismatch b={batch} t={t_len} backward={backward} bias={bias}: {e}")
383 });
384 }
385
386 #[test]
387 fn matches_the_step_by_step_recurrence() {
388 for &batch in &[1usize, 2, 3] {
389 for &t in &[1usize, 2, 5, 33] {
390 for &backward in &[false, true] {
391 for &bias in &[false, true] {
392 run_case(batch, t, backward, bias);
393 }
394 }
395 }
396 }
397 }
398
399 #[test]
401 fn carries_state_across_calls() {
402 let op =
403 GruSeq { hidden: 4, has_bias: false, chunk: 1, reset_every_turn: false, emit_y: true };
404 let x = Array3::<f32>::from_elem((1, 3, 2), 0.5);
405 let w = Array2::<f32>::from_elem((12, 2), 0.1);
406 let r = Array2::<f32>::from_elem((12, 4), 0.1);
407 let h0 = Array2::<f32>::zeros((1, 4));
408 let mk = || -> TVec<TValue> {
409 tvec!(
410 x.clone().into_tensor().into(),
411 w.clone().into_tensor().into(),
412 r.clone().into_tensor().into(),
413 h0.clone().into_tensor().into()
414 )
415 };
416 let mut carry = None;
417 let mut packed = None;
418 let first = op.eval_with(&mut carry, &mut packed, mk()).unwrap();
419 assert!(carry.is_some(), "state must be retained");
420 let second = op.eval_with(&mut carry, &mut packed, mk()).unwrap();
421 assert_ne!(
422 first[1].to_plain_array_view::<f32>().unwrap(),
423 second[1].to_plain_array_view::<f32>().unwrap(),
424 "second call must continue from the carried state, not restart from initial_h"
425 );
426 }
427
428 #[test]
432 fn reset_every_turn_restarts_from_initial_h() {
433 let op =
434 GruSeq { hidden: 4, has_bias: false, chunk: 1, reset_every_turn: true, emit_y: true };
435 let x = Array3::<f32>::from_elem((1, 3, 2), 0.5);
436 let w = Array2::<f32>::from_elem((12, 2), 0.1);
437 let r = Array2::<f32>::from_elem((12, 4), 0.1);
438 let h0 = Array2::<f32>::zeros((1, 4));
439 let mk = || -> TVec<TValue> {
440 tvec!(
441 x.clone().into_tensor().into(),
442 w.clone().into_tensor().into(),
443 r.clone().into_tensor().into(),
444 h0.clone().into_tensor().into()
445 )
446 };
447 let mut carry = None;
448 let mut packed = None;
449 let first = op.eval_with(&mut carry, &mut packed, mk()).unwrap();
450 assert!(carry.is_none(), "state must not be retained");
451 let second = op.eval_with(&mut carry, &mut packed, mk()).unwrap();
452 for slot in 0..2 {
453 assert_eq!(
454 first[slot].to_plain_array_view::<f32>().unwrap(),
455 second[slot].to_plain_array_view::<f32>().unwrap(),
456 "output {slot} must not drift between identical calls"
457 );
458 }
459 }
460}