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}