tract_core/ops/
lstm_cell.rs1use crate::internal::*;
2use tract_linalg::element_wise::ElementWise;
3use tract_linalg::routines::Func;
4
5#[derive(Debug, Clone, Hash, PartialEq, Eq)]
24pub struct LstmEpilogue {
25 pub hidden: usize,
26}
27
28impl Op for LstmEpilogue {
29 fn name(&self) -> StaticName {
30 "LstmEpilogue".into()
31 }
32
33 fn info(&self) -> TractResult<Vec<String>> {
34 Ok(vec![format!("hidden={}", self.hidden)])
35 }
36
37 op_as_typed_op!();
38}
39
40impl EvalOp for LstmEpilogue {
41 op_out_of_plan!();
42
43 fn eval(&self, _ctx: &EvalContext, inputs: TVec<TValue>) -> TractResult<TVec<TValue>> {
44 match inputs[0].datum_type().unquantized() {
50 DatumType::F32 => {
51 self.eval_t::<f32>(inputs, Func::Sigmoid.ew_f32()?, Func::Tanh.ew_f32()?)
52 }
53 DatumType::F16 => {
54 self.eval_t::<f16>(inputs, Func::Sigmoid.ew_f16()?, Func::Tanh.ew_f16()?)
55 }
56 dt => bail!("LstmEpilogue only supports f32 and f16 preactivations, got {dt:?}"),
57 }
58 }
59}
60
61impl LstmEpilogue {
62 fn eval_t<T>(
63 &self,
64 inputs: TVec<TValue>,
65 sigmoid: Box<dyn ElementWise<T>>,
66 tanh: Box<dyn ElementWise<T>>,
67 ) -> TractResult<TVec<TValue>>
68 where
69 T: Datum + Copy + std::ops::Mul<Output = T> + std::ops::Add<Output = T>,
70 {
71 let h = self.hidden;
72 let c_prev = &inputs[1]; let cp = unsafe { c_prev.as_slice_unchecked::<T>() };
74 let rows = cp.len() / h;
77 ensure!(
78 inputs[0].len() == rows * 4 * h,
79 "LstmEpilogue expects preact shaped [{rows}, 4*{h}], got {:?}",
80 inputs[0].shape()
81 );
82 let mut pre_t = inputs[0].clone().into_tensor();
84 let pre = unsafe { pre_t.as_slice_mut_unchecked::<T>() };
85 let mut ht = unsafe { Tensor::uninitialized_dt(T::datum_type(), c_prev.shape())? };
86 let mut ct = unsafe { Tensor::uninitialized_dt(T::datum_type(), c_prev.shape())? };
87 {
88 let hs = unsafe { ht.as_slice_mut_unchecked::<T>() };
89 let cs = unsafe { ct.as_slice_mut_unchecked::<T>() };
90 for r in 0..rows {
91 let pb = r * 4 * h;
92 let cb = r * h;
93 let row = &mut pre[pb..pb + 4 * h];
94 sigmoid.run(&mut row[0..3 * h])?;
96 tanh.run(&mut row[3 * h..4 * h])?;
97 for j in 0..h {
99 cs[cb + j] = row[2 * h + j] * cp[cb + j] + row[j] * row[3 * h + j];
100 }
101 hs[cb..cb + h].copy_from_slice(&cs[cb..cb + h]);
103 tanh.run(&mut hs[cb..cb + h])?;
104 for j in 0..h {
105 hs[cb + j] = hs[cb + j] * row[h + j];
106 }
107 }
108 }
109 Ok(tvec!(ht.into_tvalue(), ct.into_tvalue()))
110 }
111}
112
113impl TypedOp for LstmEpilogue {
114 as_op!();
115
116 fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
117 ensure!(inputs.len() == 2, "LstmEpilogue expects [preact, c_prev]");
118 let c_prev = inputs[1];
120 let fact = c_prev.datum_type.fact(c_prev.shape.clone());
121 Ok(tvec!(fact.clone(), fact))
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
133 fn epilogue_matches_scalar_reference() {
134 let h = 6usize;
135 let batch = 3usize;
136 let preact: Vec<f32> =
137 (0..batch * 4 * h).map(|i| ((i * 7 % 29) as f32 - 14.0) * 0.25).collect();
138 let cprev: Vec<f32> = (0..batch * h).map(|i| ((i * 5 % 17) as f32 - 8.0) * 0.2).collect();
139 let pre_t = Tensor::from_shape(&[batch, 4 * h], &preact).unwrap();
140 let cprev_t = Tensor::from_shape(&[batch, h], &cprev).unwrap();
141 let op = LstmEpilogue { hidden: h };
142 let out = op
143 .eval(&EvalContext::out_of_plan(), tvec!(pre_t.into_tvalue(), cprev_t.into_tvalue()))
144 .unwrap();
145 let ht = unsafe { out[0].as_slice_unchecked::<f32>() };
146 let ct = unsafe { out[1].as_slice_unchecked::<f32>() };
147
148 let sig = |x: f32| 1.0 / (1.0 + (-x).exp());
149 for r in 0..batch {
150 for j in 0..h {
151 let p = r * 4 * h; let it = sig(preact[p + j]);
153 let ot = sig(preact[p + h + j]);
154 let ft = sig(preact[p + 2 * h + j]);
155 let cc = preact[p + 3 * h + j].tanh();
156 let c_ref = ft * cprev[r * h + j] + it * cc;
157 let h_ref = ot * c_ref.tanh();
158 assert!((ct[r * h + j] - c_ref).abs() < 1e-3, "Ct mismatch at ({r},{j})");
159 assert!((ht[r * h + j] - h_ref).abs() < 1e-3, "Ht mismatch at ({r},{j})");
160 }
161 }
162 }
163}