Skip to main content

ruda_tensor/ops/
activation.rs

1use crate::tensor::FloatTensor;
2use crate::{Backend, Scalar, TensorMetadata, get_device_settings};
3use core::f64::consts::SQRT_2;
4
5/// Activation function operations.
6///
7/// This trait let backend implementations override activation functions for better performance.
8pub trait ActivationOps<B: Backend> {
9    /// Applies SiLU element-wise. Backends may fuse its arithmetic and rounding.
10    fn silu(tensor: FloatTensor<B>) -> FloatTensor<B> {
11        B::float_mul(tensor.clone(), B::sigmoid(tensor))
12    }
13
14    /// Applies the LeakyReLU activation function.
15    ///
16    /// # Arguments
17    ///
18    /// * `tensor` - The tensor.
19    /// * `negative_slope` - The negative_slope value that values smaller than 0 are multiplied with.
20    ///
21    /// # Returns
22    ///
23    /// The output tensor.
24    fn leaky_relu(tensor: FloatTensor<B>, negative_slope: Scalar) -> FloatTensor<B> {
25        let bool_dtype = get_device_settings::<B>(&B::float_device(&tensor)).bool_dtype;
26        let mask = B::float_lower_elem(tensor.clone(), 0f32.into(), bool_dtype);
27        let scaled_tensor = B::float_mul_scalar(tensor.clone(), negative_slope);
28
29        // Update the tensor where the values are `< 0` by `tensor * negative_slope`.
30        B::float_mask_where(tensor, mask, scaled_tensor)
31    }
32
33    /// Applies the ReLU activation function.
34    ///
35    /// # Arguments
36    ///
37    /// * `tensor` - The tensor.
38    ///
39    /// # Returns
40    ///
41    /// The output tensor.
42    fn relu(tensor: FloatTensor<B>) -> FloatTensor<B> {
43        let bool_dtype = get_device_settings::<B>(&B::float_device(&tensor)).bool_dtype;
44        let mask = B::float_lower_equal_elem(tensor.clone(), 0f32.into(), bool_dtype);
45
46        B::float_mask_fill(tensor, mask, 0f32.into())
47    }
48
49    /// Applies the ReLU activation function backward.
50    ///
51    /// # Arguments
52    ///
53    /// * `output` - The output tensor.
54    ///
55    /// # Returns
56    ///
57    /// The gradient.
58    fn relu_backward(output: FloatTensor<B>, grad: FloatTensor<B>) -> FloatTensor<B> {
59        let bool_dtype = get_device_settings::<B>(&B::float_device(&output)).bool_dtype;
60        let mask = B::float_lower_equal_elem(output, 0f32.into(), bool_dtype);
61
62        B::float_mask_fill(grad, mask, 0.into())
63    }
64
65    /// Applies the Gelu activation function.
66    ///
67    /// # Arguments
68    ///
69    /// * `tensor` - The tensor.
70    ///
71    /// # Returns
72    ///
73    /// The output tensor.
74    fn gelu(tensor: FloatTensor<B>) -> FloatTensor<B> {
75        let x = B::float_div_scalar(tensor.clone(), SQRT_2.into());
76        let x = B::float_erf(x);
77        let x = B::float_add_scalar(x, 1f32.into());
78        let x = B::float_mul(tensor, x);
79
80        B::float_div_scalar(x, 2f32.into())
81    }
82    /// Applies the PReLu activation function.
83    /// # Arguments
84    /// * `tensor` - The input tensor
85    /// * `alpha` - The weight tensor
86    fn prelu(tensor: FloatTensor<B>, alpha: FloatTensor<B>) -> FloatTensor<B> {
87        let bool_dtype = get_device_settings::<B>(&B::float_device(&tensor)).bool_dtype;
88        let mask = B::float_lower_elem(tensor.clone(), 0f32.into(), bool_dtype);
89        let scaled_tensor = B::float_mul(tensor.clone(), alpha);
90        B::float_mask_where(tensor, mask, scaled_tensor)
91    }
92
93    /// Applies the Gelu activation function backward.
94    ///
95    /// # Arguments
96    ///
97    /// * `x` - The tensor.
98    /// * `grad` - The gradient.
99    ///
100    /// # Returns
101    ///
102    /// The output tensor.
103    fn gelu_backward(x: FloatTensor<B>, grad: FloatTensor<B>) -> FloatTensor<B> {
104        // Derivative of the approximate gelu implementation based on tanh.
105
106        let constant_1 = 0.0356774;
107        let constant_2 = 0.797885;
108        let constant_3 = 0.0535161;
109        let constant_4 = 0.398942;
110
111        let x3 = B::float_powi_scalar(x.clone(), 3.into());
112
113        let c1 = B::float_mul_scalar(x3.clone(), constant_1.into());
114        let c2 = B::float_mul_scalar(x.clone(), constant_2.into());
115        let c3 = B::float_mul_scalar(x3, constant_3.into());
116        let c4 = B::float_mul_scalar(x, constant_4.into());
117
118        let inner1 = B::float_add(c1, c2);
119        let inner2 = B::float_add(c3, c4);
120
121        let tanh = B::float_tanh(inner1);
122
123        let sech = B::float_powi_scalar(tanh.clone(), 2.into());
124        let sech = B::float_neg(sech);
125        let sech = B::float_add_scalar(sech, 1.into());
126
127        let y1 = B::float_mul_scalar(tanh, 0.5.into());
128        let y2 = B::float_mul(inner2, sech);
129        let y2 = B::float_add_scalar(y2, 0.5.into());
130        let y = B::float_add(y1, y2);
131
132        B::float_mul(y, grad)
133    }
134
135    /// Applies the Sigmoid activation function.
136    ///
137    /// # Arguments
138    ///
139    /// * `tensor` - The tensor.
140    ///
141    /// # Returns
142    ///
143    /// The output tensor.
144    fn sigmoid(tensor: FloatTensor<B>) -> FloatTensor<B> {
145        let dtype = tensor.dtype();
146        let tensor_full = B::float_cast(tensor, ruda_core::tensor::FloatDType::F32);
147        let tensor_tmp = B::float_exp(B::float_neg(B::float_log(B::float_add_scalar(
148            B::float_exp(B::float_neg(tensor_full)),
149            1.0.into(),
150        ))));
151
152        B::float_cast(tensor_tmp, dtype.into())
153    }
154
155    /// Applies the Sigmoid activation function backward.
156    ///
157    /// # Arguments
158    ///
159    /// * `output` - The output tensor of the sigmoid function.
160    /// * `grad` - The gradient.
161    ///
162    /// # Returns
163    ///
164    /// The output tensor.
165    fn sigmoid_backward(output: FloatTensor<B>, grad: FloatTensor<B>) -> FloatTensor<B> {
166        let value = B::float_mul(
167            output.clone(),
168            B::float_add_scalar(B::float_neg(output), 1.0.into()),
169        );
170        B::float_mul(value, grad)
171    }
172
173    /// Applies the hard Sigmoid activation function.
174    ///
175    /// # Arguments
176    ///
177    /// * `tensor` - The tensor.
178    /// * `alpha` - The alpha value that the tensor is multiplied with.
179    /// * `beta` - The beta value that is added to the tensor
180    ///
181    /// # Returns
182    ///
183    /// The output tensor.
184    fn hard_sigmoid(tensor: FloatTensor<B>, alpha: Scalar, beta: Scalar) -> FloatTensor<B> {
185        let dtype = tensor.dtype();
186        let tensor_full = B::float_cast(tensor, ruda_core::tensor::FloatDType::F32);
187
188        let tensor_tmp = B::float_clamp(
189            B::float_add_scalar(B::float_mul_scalar(tensor_full, alpha), beta),
190            0.0.into(),
191            1.0.into(),
192        );
193
194        B::float_cast(tensor_tmp, dtype.into())
195    }
196
197    /// Applies the LogSigmoid activation function.
198    ///
199    /// # Arguments
200    ///
201    /// * `tensor` - The tensor.
202    ///
203    /// # Returns
204    ///
205    /// The output tensor.
206    fn log_sigmoid(tensor: FloatTensor<B>) -> FloatTensor<B> {
207        // To avoid overflow, we use the log-sum-exp trick.
208        //
209        // ```ignore
210        // log(sigmoid(x)) = log(1/(1 + exp(-x)))
211        //                 = log(1) - log(1 + exp(-x))
212        //                 = -log(1 + exp(-x))
213        //                 = -log(exp(0) + exp(-x))
214        // ```
215        // The `exp(t)` of even a moderate-magnitude positive number can be astronomically huge, so we
216        // subtract the `max(t, 0)` of each value (where `t = -x` in this case). This results in the
217        // following equivalence:
218        // ```ignore
219        // log(sigmoid(x)) = -(max(-x, 0) + log(exp(-max(-x, 0)) + exp(-x - max(-x, 0))))
220        // ```
221        //
222        // This extends the range of values for which we obtain accurate results.
223
224        // max(-x, 0)
225        let bool_dtype = get_device_settings::<B>(&B::float_device(&tensor)).bool_dtype;
226        let tensor_neg = B::float_neg(tensor);
227        let mask = B::float_lower_elem(tensor_neg.clone(), 0f32.into(), bool_dtype);
228        let max_elem = B::float_mask_fill(tensor_neg.clone(), mask, 0f32.into());
229        let max_elem_neg = B::float_neg(max_elem.clone());
230
231        // z = exp(-max(-x, 0)) + exp(-x - max(-x, 0))
232        let z = B::float_add(
233            B::float_exp(max_elem_neg.clone()),
234            B::float_exp(B::float_sub(tensor_neg, max_elem.clone())),
235        );
236
237        // -max(-x, 0) - log(-z)
238        B::float_sub(max_elem_neg, B::float_log(z))
239    }
240
241    /// Applies the softmax function along the given dimension.
242    ///
243    /// Uses the max-shift trick for numerical stability: the per-row `max` is detached
244    /// so no gradient flows back through it (the shift is a numerical-stability
245    /// transformation, not part of the function).
246    ///
247    /// # Arguments
248    ///
249    /// * `tensor` - The tensor.
250    /// * `dim` - The dimension along which softmax is computed.
251    ///
252    /// # Returns
253    ///
254    /// The output tensor.
255    fn softmax(tensor: FloatTensor<B>, dim: usize) -> FloatTensor<B> {
256        let max = B::float_max_dim(B::float_detach(tensor.clone()), dim);
257        let shifted = B::float_sub(tensor, max);
258        let exp = B::float_exp(shifted);
259        let sum = B::float_sum_dim(exp.clone(), dim);
260        B::float_div(exp, sum)
261    }
262
263    /// Applies the log-softmax function along the given dimension.
264    ///
265    /// Computed via the log-sum-exp trick with a detached max-shift for numerical
266    /// stability.
267    ///
268    /// # Arguments
269    ///
270    /// * `tensor` - The tensor.
271    /// * `dim` - The dimension along which log-softmax is computed.
272    ///
273    /// # Returns
274    ///
275    /// The output tensor.
276    fn log_softmax(tensor: FloatTensor<B>, dim: usize) -> FloatTensor<B> {
277        let max = B::float_max_dim(B::float_detach(tensor.clone()), dim);
278        let shifted = B::float_sub(tensor, max);
279        let log_sum_exp = B::float_log(B::float_sum_dim(B::float_exp(shifted.clone()), dim));
280        B::float_sub(shifted, log_sum_exp)
281    }
282
283    /// Applies the softmin function along the given dimension.
284    ///
285    /// Equivalent to `softmax(-tensor, dim)`.
286    ///
287    /// # Arguments
288    ///
289    /// * `tensor` - The tensor.
290    /// * `dim` - The dimension along which softmin is computed.
291    ///
292    /// # Returns
293    ///
294    /// The output tensor.
295    fn softmin(tensor: FloatTensor<B>, dim: usize) -> FloatTensor<B> {
296        Self::softmax(B::float_neg(tensor), dim)
297    }
298
299    /// Applies the LogSigmoid activation function backward.
300    ///
301    /// # Arguments
302    ///
303    /// * `x` - The input tensor.
304    /// * `grad` - The gradient.
305    ///
306    /// # Returns
307    ///
308    /// The output gradient.
309    fn log_sigmoid_backward(x: FloatTensor<B>, grad: FloatTensor<B>) -> FloatTensor<B> {
310        // Derivative of -max(-x, 0) - log(exp(-max(-x, 0)) - exp(-x - max(-x, 0)))) is
311        // -max_derive - (-max_derive * exp(-max(-x, 0)) + (-1 - max_derive) * exp(-x - max(-x, 0))) / z
312        // where z = exp(-max(-x, 0)) + exp(-x - max(-x, 0))
313        //
314        // This simplifies to:
315        // -max_derive - (z-1)/z if x is >= 0
316        // -max_derive + (z-1)/z if x is < 0
317
318        let shape = x.shape();
319        let dtype = x.dtype();
320        let device = B::float_device(&x);
321        let bool_dtype = get_device_settings::<B>(&device).bool_dtype;
322
323        // max(-x, 0)
324        let x_neg = B::float_neg(x);
325        let mask = B::float_lower_elem(x_neg.clone(), 0f32.into(), bool_dtype); // -x < 0 or x >= 0
326        let max_elem = B::float_mask_fill(x_neg.clone(), mask.clone(), 0f32.into());
327
328        // z = exp(-max(-x, 0)) + exp(-x - max(-x, 0))
329        let z = B::float_add(
330            B::float_exp(B::float_neg(max_elem.clone())),
331            B::float_exp(B::float_sub(x_neg, max_elem)),
332        );
333
334        // Derivative of max(-x, 0) is 1 if x < 0 or 0 if x >= 0
335        let ones = B::float_ones(shape, &device, dtype.into());
336        let max_derive = B::float_mask_fill(ones.clone(), mask.clone(), 0f32.into());
337        let sign = B::float_mask_fill(ones.clone(), mask, (-1f32).into());
338
339        // grad * (max_derive - sign * (1 - (1 / z)))
340        B::float_mul(
341            grad,
342            B::float_sub(
343                max_derive,
344                B::float_mul(sign, B::float_sub(ones, B::float_recip(z))),
345            ),
346        )
347    }
348}