tract_core/ops/
gru_cell.rs1use crate::internal::*;
2use tract_linalg::element_wise::ElementWise;
3use tract_linalg::routines::Func;
4
5#[derive(Debug, Clone, Hash, PartialEq, Eq)]
30pub struct GruEpilogue {
31 pub hidden: usize,
32}
33
34impl Op for GruEpilogue {
35 fn name(&self) -> StaticName {
36 "GruEpilogue".into()
37 }
38
39 fn info(&self) -> TractResult<Vec<String>> {
40 Ok(vec![format!("hidden={}", self.hidden)])
41 }
42
43 op_as_typed_op!();
44}
45
46impl EvalOp for GruEpilogue {
47 op_out_of_plan!();
48
49 fn eval(&self, _ctx: &EvalContext, inputs: TVec<TValue>) -> TractResult<TVec<TValue>> {
50 match inputs[0].datum_type().unquantized() {
51 DatumType::F32 => {
52 self.eval_t::<f32>(inputs, Func::Sigmoid.ew_f32()?, Func::Tanh.ew_f32()?)
53 }
54 DatumType::F16 => {
55 self.eval_t::<f16>(inputs, Func::Sigmoid.ew_f16()?, Func::Tanh.ew_f16()?)
56 }
57 dt => bail!("GruEpilogue only supports f32 and f16 preactivations, got {dt:?}"),
58 }
59 }
60}
61
62#[allow(clippy::too_many_arguments)]
69pub fn gru_cell_rows<T>(
70 h: usize,
71 rows: usize,
72 xh: &mut [T],
73 rh: &[T],
74 h_prev: &[T],
75 h_out: &mut [T],
76 sigmoid: &dyn ElementWise<T>,
77 tanh: &dyn ElementWise<T>,
78) -> TractResult<()>
79where
80 T: Datum
81 + Copy
82 + std::ops::Mul<Output = T>
83 + std::ops::Add<Output = T>
84 + std::ops::Sub<Output = T>,
85{
86 for row in 0..rows {
87 let gb = row * 3 * h;
88 let hb = row * h;
89 let g = &mut xh[gb..gb + 3 * h];
90 let r = &rh[gb..gb + 3 * h];
91 for j in 0..2 * h {
92 g[j] = g[j] + r[j];
93 }
94 sigmoid.run(&mut g[0..2 * h])?;
95 for j in 0..h {
96 g[2 * h + j] = g[2 * h + j] + g[h + j] * r[2 * h + j];
97 }
98 tanh.run(&mut g[2 * h..3 * h])?;
99 for j in 0..h {
100 let cand = g[2 * h + j];
101 h_out[hb + j] = cand + g[j] * (h_prev[hb + j] - cand);
102 }
103 }
104 Ok(())
105}
106
107impl GruEpilogue {
108 fn eval_t<T>(
109 &self,
110 inputs: TVec<TValue>,
111 sigmoid: Box<dyn ElementWise<T>>,
112 tanh: Box<dyn ElementWise<T>>,
113 ) -> TractResult<TVec<TValue>>
114 where
115 T: Datum
116 + Copy
117 + std::ops::Mul<Output = T>
118 + std::ops::Add<Output = T>
119 + std::ops::Sub<Output = T>,
120 {
121 let h = self.hidden;
122 let h_prev = &inputs[2];
123 let hp = unsafe { h_prev.as_slice_unchecked::<T>() };
124 let rows = hp.len() / h;
127 ensure!(
128 inputs[0].len() == rows * 3 * h && inputs[1].len() == rows * 3 * h,
129 "GruEpilogue expects xh and rh shaped [{rows}, 3*{h}], got {:?} and {:?}",
130 inputs[0].shape(),
131 inputs[1].shape()
132 );
133 let rh = unsafe { inputs[1].as_slice_unchecked::<T>() };
134 let mut acc_t = inputs[0].clone().into_tensor();
135 let acc = unsafe { acc_t.as_slice_mut_unchecked::<T>() };
136 let mut ht = unsafe { Tensor::uninitialized_dt(T::datum_type(), h_prev.shape())? };
137 {
138 let hs = unsafe { ht.as_slice_mut_unchecked::<T>() };
139 gru_cell_rows(h, rows, acc, rh, hp, hs, &*sigmoid, &*tanh)?;
140 }
141 Ok(tvec!(ht.into_tvalue()))
142 }
143}
144
145impl TypedOp for GruEpilogue {
146 as_op!();
147
148 fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
149 ensure!(inputs.len() == 3, "GruEpilogue expects [xh, rh, h_prev]");
150 ensure!(
151 inputs[0].datum_type == inputs[2].datum_type,
152 "GruEpilogue gate and state datum types differ: {:?} and {:?}",
153 inputs[0].datum_type,
154 inputs[2].datum_type
155 );
156 let h_prev = inputs[2];
157 Ok(tvec!(h_prev.datum_type.fact(h_prev.shape.clone())))
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
171 fn epilogue_matches_scalar_reference() {
172 let h = 4usize;
173 let batch = 3usize;
174 let xh: Vec<f32> =
175 (0..batch * 3 * h).map(|i| ((i * 7 % 29) as f32 - 14.0) * 0.25).collect();
176 let rh: Vec<f32> =
177 (0..batch * 3 * h).map(|i| ((i * 11 % 23) as f32 - 11.0) * 0.3).collect();
178 let hprev: Vec<f32> = (0..batch * h).map(|i| ((i * 5 % 17) as f32 - 8.0) * 0.2).collect();
179 let xh_t = Tensor::from_shape(&[batch, 3 * h], &xh).unwrap();
180 let rh_t = Tensor::from_shape(&[batch, 3 * h], &rh).unwrap();
181 let hprev_t = Tensor::from_shape(&[batch, h], &hprev).unwrap();
182 let op = GruEpilogue { hidden: h };
183 let out = op
184 .eval(
185 &EvalContext::out_of_plan(),
186 tvec!(xh_t.into_tvalue(), rh_t.into_tvalue(), hprev_t.into_tvalue()),
187 )
188 .unwrap();
189 let got = unsafe { out[0].as_slice_unchecked::<f32>() };
190
191 let sig = |x: f32| 1.0 / (1.0 + (-x).exp());
192 for r in 0..batch {
193 for j in 0..h {
194 let p = r * 3 * h; let zt = sig(xh[p + j] + rh[p + j]);
196 let rt = sig(xh[p + h + j] + rh[p + h + j]);
197 let ht = (xh[p + 2 * h + j] + rt * rh[p + 2 * h + j]).tanh();
198 let h_ref = (1.0 - zt) * ht + zt * hprev[r * h + j];
199 assert!((got[r * h + j] - h_ref).abs() < 1e-3, "Ht mismatch at ({r},{j})");
200 }
201 }
202 }
203}