Skip to main content

burn_backend/backend/ops/modules/
base.rs

1use super::{attention, conv, ctc, linear, pool};
2use crate::ops::unfold::{create_unfolding_weight, unfold4d_using_conv2d};
3use crate::tensor::{BoolTensor, FloatTensor, IntTensor};
4use crate::{Backend, Scalar, TensorMetadata};
5#[allow(deprecated)]
6pub use burn_std::ops::{
7    AttentionModuleOptions, AvgPoolOptions, CausalAlignment, ConvOptions, ConvTransposeOptions,
8    CoordinateTransformMode, CubicOptions, DeformConvOptions, GridSampleOptions,
9    GridSamplePaddingMode, InterpolateMode, InterpolateOptions, MaxPoolOptions,
10    NearestRoundingMode, PadMode, PaddedConvOptions, UnfoldOptions,
11};
12use burn_std::{IndexingUpdateOp, IntDType, Shape};
13
14/// Gradients computed during the backward pass for each input of
15/// [attention_with_logsumexp](ModuleOps::attention_with_logsumexp).
16#[derive(new)]
17pub struct AttentionBackward<B: Backend> {
18    /// Query gradient, `[batch_size, num_heads, seq_len_q, head_dim]`.
19    pub query_grad: FloatTensor<B>,
20
21    /// Key gradient, `[batch_size, num_kv_heads, seq_len_k, head_dim]`: every query head of a
22    /// group summed into its key head.
23    pub key_grad: FloatTensor<B>,
24
25    /// Value gradient, `[batch_size, num_kv_heads, seq_len_k, val_dim]`.
26    pub value_grad: FloatTensor<B>,
27}
28
29/// Gradient computed during the backward pass for each tensor used by [conv2d](ModuleOps::conv2d).
30#[derive(new)]
31pub struct Conv2dBackward<B: Backend> {
32    /// Gradient.
33    pub x_grad: FloatTensor<B>,
34
35    /// Weights gradient.
36    pub weights_grad: FloatTensor<B>,
37
38    /// Bias gradient.
39    pub bias_grad: Option<FloatTensor<B>>,
40}
41
42/// Gradient computed during the backward pass for each tensor used by [deform_conv2d](ModuleOps::deform_conv2d).
43#[derive(new)]
44pub struct DeformConv2dBackward<B: Backend> {
45    /// Gradient.
46    pub x_grad: FloatTensor<B>,
47
48    /// Offset gradient.
49    pub offset_grad: FloatTensor<B>,
50
51    /// Weights gradient.
52    pub weight_grad: FloatTensor<B>,
53
54    /// Mask gradient.
55    pub mask_grad: Option<FloatTensor<B>>,
56
57    /// Bias gradient.
58    pub bias_grad: Option<FloatTensor<B>>,
59}
60
61/// Gradient computed during the backward pass for each tensor used by [conv3d](ModuleOps::conv3d).
62#[derive(new)]
63pub struct Conv3dBackward<B: Backend> {
64    /// Gradient.
65    pub x_grad: FloatTensor<B>,
66
67    /// Weights gradient.
68    pub weights_grad: FloatTensor<B>,
69
70    /// Bias gradient.
71    pub bias_grad: Option<FloatTensor<B>>,
72}
73
74/// Gradient computed during the backward pass for each tensor used by [max_pool1d](ModuleOps::max_pool1d).
75#[derive(new)]
76pub struct MaxPool1dBackward<B: Backend> {
77    /// Gradient.
78    pub x_grad: FloatTensor<B>,
79}
80
81/// Results from [max_pool1d](ModuleOps::max_pool1d_with_indices).
82#[derive(new)]
83pub struct MaxPool1dWithIndices<B: Backend> {
84    /// The output tensor.
85    pub output: FloatTensor<B>,
86
87    /// The indices tensor.
88    pub indices: IntTensor<B>,
89}
90
91/// Gradient computed during the backward pass for each tensor used by [max_pool2d](ModuleOps::max_pool2d).
92#[derive(new)]
93pub struct MaxPool2dBackward<B: Backend> {
94    /// Gradient.
95    pub x_grad: FloatTensor<B>,
96}
97
98/// Results from [batch_norm_train](ModuleOps::batch_norm_train).
99#[derive(new)]
100pub struct BatchNormTrain<B: Backend> {
101    /// The normalized input.
102    pub output: FloatTensor<B>,
103
104    /// The batch mean per channel, `[channels]`.
105    pub mean: FloatTensor<B>,
106
107    /// The biased batch variance per channel, `[channels]`.
108    pub variance: FloatTensor<B>,
109}
110
111/// Gradient computed during the backward pass for each tensor used by
112/// [batch_norm_train](ModuleOps::batch_norm_train).
113#[derive(new)]
114pub struct BatchNormTrainBackward<B: Backend> {
115    /// Gradient of the input.
116    pub x_grad: FloatTensor<B>,
117
118    /// Gradient of `gamma`, `[channels]`.
119    pub gamma_grad: FloatTensor<B>,
120
121    /// Gradient of `beta`, `[channels]`.
122    pub beta_grad: FloatTensor<B>,
123}
124
125/// Results from [max_pool2d](ModuleOps::max_pool2d_with_indices).
126#[derive(new)]
127pub struct MaxPool2dWithIndices<B: Backend> {
128    /// The output tensor.
129    pub output: FloatTensor<B>,
130
131    /// The indices tensor.
132    pub indices: IntTensor<B>,
133}
134
135/// Gradient computed during the backward pass for each tensor used by [max_pool3d](ModuleOps::max_pool3d).
136#[derive(new, Debug, Clone)]
137pub struct MaxPool3dBackward<B: Backend> {
138    /// Gradient.
139    pub x_grad: FloatTensor<B>,
140}
141
142/// Results from [max_pool3d](ModuleOps::max_pool3d_with_indices).
143#[derive(new, Debug, Clone)]
144pub struct MaxPool3dWithIndices<B: Backend> {
145    /// The output tensor.
146    pub output: FloatTensor<B>,
147
148    /// The indices tensor.
149    pub indices: IntTensor<B>,
150}
151
152/// Gradient computed during the backward pass for each tensor used by [interpolate](ModuleOps::interpolate).
153#[derive(new)]
154pub struct InterpolateBackward<B: Backend> {
155    /// Gradient.
156    pub x_grad: FloatTensor<B>,
157}
158
159/// Module operations trait.
160pub trait ModuleOps<B: Backend> {
161    /// Applies batch normalization using explicitly supplied channel statistics.
162    ///
163    /// The input has shape `[batch, channels, ...]`; all other tensors have
164    /// shape `[channels]`.
165    ///
166    /// This operation doesn't calculate or update statistics. Callers may
167    /// supply running statistics for inference or batch statistics calculated
168    /// by a training path.
169    fn batch_norm(
170        x: FloatTensor<B>,
171        gamma: FloatTensor<B>,
172        beta: FloatTensor<B>,
173        mean: FloatTensor<B>,
174        variance: FloatTensor<B>,
175        epsilon: f64,
176    ) -> FloatTensor<B> {
177        let rank = x.shape().num_dims();
178        let channels = x.shape()[1];
179        let mut dimensions = alloc::vec![1; rank];
180        dimensions[1] = channels;
181        let shape = Shape::from(dimensions);
182        let gamma = B::float_reshape(gamma, shape.clone());
183        let beta = B::float_reshape(beta, shape.clone());
184        let mean = B::float_reshape(mean, shape.clone());
185        let variance = B::float_reshape(variance, shape);
186        let std = B::float_sqrt(B::float_add_scalar(variance, Scalar::Float(epsilon)));
187        let normalized = B::float_div(B::float_sub(x, mean), std);
188        B::float_add(B::float_mul(normalized, gamma), beta)
189    }
190
191    /// Applies batch normalization with the statistics of the batch itself,
192    /// and returns them.
193    ///
194    /// The input has shape `[batch, channels, ...]`; `gamma` and `beta` have
195    /// shape `[channels]`, as do the returned mean and biased variance — what
196    /// a training path feeds its running statistics.
197    ///
198    /// An autodiff backend differentiates this as one operation, through
199    /// [batch_norm_train_backward](ModuleOps::batch_norm_train_backward),
200    /// rather than through the reductions and broadcasts that computing the
201    /// statistics out of tensor operations would record.
202    fn batch_norm_train(
203        x: FloatTensor<B>,
204        gamma: FloatTensor<B>,
205        beta: FloatTensor<B>,
206        epsilon: f64,
207    ) -> BatchNormTrain<B> {
208        let shape = x.shape();
209        let channels = shape[1];
210        let samples_per_channel = shape.num_elements() / channels;
211        let flattened = Shape::new([channels, samples_per_channel]);
212        let mut per_channel = alloc::vec![1; shape.num_dims()];
213        per_channel[1] = channels;
214
215        // Use mean directly to avoid overflowing intermediate sums in f16.
216        let mean = B::float_mean_dim(
217            B::float_reshape(B::float_swap_dims(x.clone(), 0, 1), flattened.clone()),
218            1,
219        );
220        let mean = B::float_reshape(mean, Shape::from(per_channel));
221        let centered = B::float_sub(x.clone(), mean.clone());
222        let variance = B::float_mean_dim(
223            B::float_reshape(
224                B::float_swap_dims(B::float_mul(centered.clone(), centered), 0, 1),
225                flattened,
226            ),
227            1,
228        );
229        let mean = B::float_reshape(mean, Shape::new([channels]));
230        let variance = B::float_reshape(variance, Shape::new([channels]));
231        let output = B::batch_norm(x, gamma, beta, mean.clone(), variance.clone(), epsilon);
232
233        BatchNormTrain::new(output, mean, variance)
234    }
235
236    /// Gradients of [batch_norm_train](ModuleOps::batch_norm_train) with
237    /// respect to its input, `gamma` and `beta`, given the gradient of its
238    /// output. `mean` and `variance` are the statistics it returned.
239    fn batch_norm_train_backward(
240        x: FloatTensor<B>,
241        gamma: FloatTensor<B>,
242        mean: FloatTensor<B>,
243        variance: FloatTensor<B>,
244        epsilon: f64,
245        output_grad: FloatTensor<B>,
246    ) -> BatchNormTrainBackward<B> {
247        let shape = x.shape();
248        let rank = shape.num_dims();
249        let channels = shape[1];
250        let flattened = Shape::new([channels, shape.num_elements() / channels]);
251        let mut per_channel = alloc::vec![1; rank];
252        per_channel[1] = channels;
253        let per_channel = Shape::from(per_channel);
254
255        let inv_std = B::float_reshape(
256            B::float_recip(B::float_sqrt(B::float_add_scalar(
257                variance,
258                Scalar::Float(epsilon),
259            ))),
260            per_channel.clone(),
261        );
262        let normalized = B::float_mul(
263            B::float_sub(x, B::float_reshape(mean, per_channel.clone())),
264            inv_std.clone(),
265        );
266
267        let output_grad_flat = B::float_reshape(
268            B::float_swap_dims(output_grad.clone(), 0, 1),
269            flattened.clone(),
270        );
271        let normalized_grad_flat = B::float_reshape(
272            B::float_swap_dims(B::float_mul(output_grad.clone(), normalized.clone()), 0, 1),
273            flattened,
274        );
275        let beta_grad = B::float_sum_dim(output_grad_flat.clone(), 1);
276        let gamma_grad = B::float_sum_dim(normalized_grad_flat.clone(), 1);
277
278        // Compute means independently: parameter-gradient sums can overflow in f16.
279        let mean_grad =
280            B::float_reshape(B::float_mean_dim(output_grad_flat, 1), per_channel.clone());
281        let mean_normalized_grad = B::float_reshape(
282            B::float_mean_dim(normalized_grad_flat, 1),
283            per_channel.clone(),
284        );
285        let centred_grad = B::float_sub(output_grad, mean_grad);
286        let projected = B::float_mul(normalized, mean_normalized_grad);
287        let scale = B::float_mul(B::float_reshape(gamma, per_channel), inv_std);
288        let x_grad = B::float_mul(scale, B::float_sub(centred_grad, projected));
289
290        BatchNormTrainBackward::new(
291            x_grad,
292            B::float_reshape(gamma_grad, Shape::new([channels])),
293            B::float_reshape(beta_grad, Shape::new([channels])),
294        )
295    }
296
297    /// Embedding operation.
298    ///
299    /// # Arguments
300    ///
301    /// * `weights` - The embedding weights.
302    /// * `indices` - The indices tensor.
303    ///
304    /// # Returns
305    ///
306    /// The output tensor.
307    fn embedding(weights: FloatTensor<B>, indices: IntTensor<B>) -> FloatTensor<B> {
308        let [batch_size, seq_length] = indices.shape().dims();
309        let [_, d_model] = weights.shape().dims();
310
311        let indices = B::int_reshape(indices, Shape::new([batch_size * seq_length]));
312        let output = B::float_select(weights, 0, indices);
313
314        B::float_reshape(output, Shape::new([batch_size, seq_length, d_model]))
315    }
316
317    /// Embedding backward operation.
318    ///
319    /// # Arguments
320    ///
321    /// * `weights` - The embedding weights.
322    /// * `output_grad` - The output gradient.
323    /// * `indices` - The indices tensor.
324    ///
325    /// # Returns
326    ///
327    /// The gradient.
328    fn embedding_backward(
329        weights: FloatTensor<B>,
330        output_grad: FloatTensor<B>,
331        indices: IntTensor<B>,
332    ) -> FloatTensor<B> {
333        let [batch_size, seq_length] = indices.shape().dims();
334        let [n_embeddings, d_model] = weights.shape().dims();
335        let device = weights.device();
336        let dtype = output_grad.dtype();
337
338        let indices = B::int_reshape(indices, Shape::new([batch_size * seq_length]));
339        let output_grad =
340            B::float_reshape(output_grad, Shape::new([batch_size * seq_length, d_model]));
341        let grad = B::float_zeros(Shape::new([n_embeddings, d_model]), &device, dtype.into());
342
343        B::float_select_assign(grad, 0, indices, output_grad, IndexingUpdateOp::Add)
344    }
345
346    /// Linear transformation.
347    ///
348    /// # Shapes
349    ///
350    /// x:      `[..., d_input]`,
351    /// weight: `[d_input, d_output]`,
352    /// bias:   `[d_output]`,
353    fn linear(
354        x: FloatTensor<B>,
355        weight: FloatTensor<B>,
356        bias: Option<FloatTensor<B>>,
357    ) -> FloatTensor<B> {
358        linear::linear::<B>(x, weight, bias)
359    }
360    /// Backward pass for [linear](ModuleOps::linear), returning the gradient for `x`.
361    fn linear_x_backward(weight: FloatTensor<B>, output_grad: FloatTensor<B>) -> FloatTensor<B> {
362        linear::linear_x_backward::<B>(weight, output_grad)
363    }
364    /// Backward pass for [linear](ModuleOps::linear), returning the gradient for `weight`.
365    fn linear_weight_backward(x: FloatTensor<B>, output_grad: FloatTensor<B>) -> FloatTensor<B> {
366        linear::linear_weight_backward::<B>(x, output_grad)
367    }
368    /// Backward pass for [linear](ModuleOps::linear), returning the gradient for `bias`.
369    fn linear_bias_backward(output_grad: FloatTensor<B>) -> FloatTensor<B> {
370        linear::linear_bias_backward::<B>(output_grad)
371    }
372
373    /// One dimensional convolution.
374    ///
375    /// # Shapes
376    ///
377    /// x:      `[batch_size, channels_in, length]`,
378    /// weight: `[channels_out, channels_in, kernel_size]`,
379    /// bias:   `[channels_out]`,
380    fn conv1d(
381        x: FloatTensor<B>,
382        weight: FloatTensor<B>,
383        bias: Option<FloatTensor<B>>,
384        options: ConvOptions<1>,
385    ) -> FloatTensor<B> {
386        conv::conv1d_from_conv2d::<B>(x, weight, bias, options)
387    }
388    /// Backward pass for the [conv1d](ModuleOps::conv1d) operation, returning the gradient for `x`.
389    fn conv1d_x_backward(
390        x: FloatTensor<B>,
391        weight: FloatTensor<B>,
392        output_grad: FloatTensor<B>,
393        options: ConvOptions<1>,
394    ) -> FloatTensor<B> {
395        conv::conv1d_x_backward::<B>(x, weight, output_grad, options)
396    }
397    /// Backward pass for the [conv1d](ModuleOps::conv1d) operation, returning the gradient for `weight`.
398    fn conv1d_weight_backward(
399        x: FloatTensor<B>,
400        weight: FloatTensor<B>,
401        output_grad: FloatTensor<B>,
402        options: ConvOptions<1>,
403    ) -> FloatTensor<B> {
404        conv::conv1d_weight_backward::<B>(x, weight, output_grad, options)
405    }
406    /// Backward pass for the [conv1d](ModuleOps::conv1d) operation, returning the gradient for `bias`.
407    fn conv1d_bias_backward(
408        x: FloatTensor<B>,
409        bias: FloatTensor<B>,
410        output_grad: FloatTensor<B>,
411    ) -> FloatTensor<B> {
412        conv::conv1d_bias_backward::<B>(x, bias, output_grad)
413    }
414    /// Two dimensional convolution.
415    ///
416    /// # Shapes
417    ///
418    /// x:      `[batch_size, channels_in, height, width]`,
419    /// weight: `[channels_out, channels_in, kernel_size_1, kernel_size_2]`,
420    /// bias:   `[channels_out]`,
421    fn conv2d(
422        x: FloatTensor<B>,
423        weight: FloatTensor<B>,
424        bias: Option<FloatTensor<B>>,
425        options: ConvOptions<2>,
426    ) -> FloatTensor<B>;
427    /// Backward pass for the [conv2d](ModuleOps::conv2d) operation, returning the gradient for `x`.
428    fn conv2d_x_backward(
429        x: FloatTensor<B>,
430        weight: FloatTensor<B>,
431        output_grad: FloatTensor<B>,
432        options: ConvOptions<2>,
433    ) -> FloatTensor<B> {
434        conv::conv2d_x_backward::<B>(x, weight, output_grad, options)
435    }
436    /// Backward pass for the [conv2d](ModuleOps::conv2d) operation, returning the gradient for `weight`.
437    fn conv2d_weight_backward(
438        x: FloatTensor<B>,
439        weight: FloatTensor<B>,
440        output_grad: FloatTensor<B>,
441        options: ConvOptions<2>,
442    ) -> FloatTensor<B> {
443        conv::conv2d_weight_backward::<B>(x, weight, output_grad, options)
444    }
445    /// Backward pass for the [conv2d](ModuleOps::conv2d) operation, returning the gradient for `bias`.
446    fn conv2d_bias_backward(
447        x: FloatTensor<B>,
448        bias: FloatTensor<B>,
449        output_grad: FloatTensor<B>,
450    ) -> FloatTensor<B> {
451        conv::conv2d_bias_backward::<B>(x, bias, output_grad)
452    }
453
454    /// Two dimensional deformable convolution.
455    ///
456    /// # Shapes
457    ///
458    /// x:      `[batch_size, channels_in, height, width]`,
459    /// weight: `[channels_out, channels_in, kernel_size_1, kernel_size_2]`,
460    /// bias:   `[channels_out]`,
461    fn deform_conv2d(
462        x: FloatTensor<B>,
463        offset: FloatTensor<B>,
464        weight: FloatTensor<B>,
465        mask: Option<FloatTensor<B>>,
466        bias: Option<FloatTensor<B>>,
467        options: DeformConvOptions<2>,
468    ) -> FloatTensor<B>;
469    /// Backward pass for the [deform_conv2d](ModuleOps::deform_conv2d) operation.
470    fn deform_conv2d_backward(
471        x: FloatTensor<B>,
472        offset: FloatTensor<B>,
473        weight: FloatTensor<B>,
474        mask: Option<FloatTensor<B>>,
475        bias: Option<FloatTensor<B>>,
476        output_grad: FloatTensor<B>,
477        options: DeformConvOptions<2>,
478    ) -> DeformConv2dBackward<B>;
479
480    /// Three dimensional convolution.
481    ///
482    /// # Shapes
483    ///
484    /// x:      `[batch_size, channels_in, depth, height, width]`,
485    /// weight: `[channels_out, channels_in, kernel_size_1, kernel_size_2, kernel_size_3]`,
486    /// bias:   `[channels_out]`,
487    fn conv3d(
488        x: FloatTensor<B>,
489        weight: FloatTensor<B>,
490        bias: Option<FloatTensor<B>>,
491        options: ConvOptions<3>,
492    ) -> FloatTensor<B>;
493    /// Backward pass for the [conv3d](ModuleOps::conv3d) operation, returning the gradient for `x`.
494    fn conv3d_x_backward(
495        x: FloatTensor<B>,
496        weight: FloatTensor<B>,
497        output_grad: FloatTensor<B>,
498        options: ConvOptions<3>,
499    ) -> FloatTensor<B> {
500        conv::conv3d_x_backward::<B>(x, weight, output_grad, options)
501    }
502    /// Backward pass for the [conv3d](ModuleOps::conv3d) operation, returning the gradient for `weight`.
503    fn conv3d_weight_backward(
504        x: FloatTensor<B>,
505        weight: FloatTensor<B>,
506        output_grad: FloatTensor<B>,
507        options: ConvOptions<3>,
508    ) -> FloatTensor<B> {
509        conv::conv3d_weight_backward::<B>(x, weight, output_grad, options)
510    }
511    /// Backward pass for the [conv3d](ModuleOps::conv3d) operation, returning the gradient for `bias`.
512    fn conv3d_bias_backward(
513        x: FloatTensor<B>,
514        bias: FloatTensor<B>,
515        output_grad: FloatTensor<B>,
516    ) -> FloatTensor<B> {
517        conv::conv3d_bias_backward::<B>(x, bias, output_grad)
518    }
519    /// One dimensional transposed convolution.
520    ///
521    /// # Shapes
522    ///
523    /// x:      `[batch_size, channels_in, length]`,
524    /// weight: `[channels_in, channels_out, length]`,
525    /// bias:   `[channels_out]`,
526    fn conv_transpose1d(
527        x: FloatTensor<B>,
528        weight: FloatTensor<B>,
529        bias: Option<FloatTensor<B>>,
530        options: ConvTransposeOptions<1>,
531    ) -> FloatTensor<B> {
532        conv::conv_transpose1d_from_conv_transpose2d::<B>(x, weight, bias, options)
533    }
534    /// Backward pass for the [conv transpose 1d](ModuleOps::conv_transpose1d) operation, returning the gradient for `x`.
535    fn conv_transpose1d_x_backward(
536        weight: FloatTensor<B>,
537        output_grad: FloatTensor<B>,
538        options: ConvTransposeOptions<1>,
539    ) -> FloatTensor<B> {
540        conv::conv_transpose1d_x_backward::<B>(weight, output_grad, options)
541    }
542    /// Backward pass for the [conv transpose 1d](ModuleOps::conv_transpose1d) operation, returning the gradient for `weight`.
543    fn conv_transpose1d_weight_backward(
544        x: FloatTensor<B>,
545        weight: FloatTensor<B>,
546        output_grad: FloatTensor<B>,
547        options: ConvTransposeOptions<1>,
548    ) -> FloatTensor<B> {
549        conv::conv_transpose1d_weight_backward::<B>(x, weight, output_grad, options)
550    }
551    /// Backward pass for the [conv transpose 1d](ModuleOps::conv_transpose1d) operation, returning the gradient for `bias`.
552    fn conv_transpose1d_bias_backward(
553        x: FloatTensor<B>,
554        bias: FloatTensor<B>,
555        output_grad: FloatTensor<B>,
556    ) -> FloatTensor<B> {
557        conv::conv_transpose1d_bias_backward::<B>(x, bias, output_grad)
558    }
559
560    /// Two dimensional transposed convolution.
561    ///
562    /// # Shapes
563    ///
564    /// x:      `[batch_size, channels_in, height, width]`,
565    /// weight: `[channels_in, channels_out, kernel_size_1, kernel_size_2]`,
566    /// bias:   `[channels_out]`,
567    fn conv_transpose2d(
568        x: FloatTensor<B>,
569        weight: FloatTensor<B>,
570        bias: Option<FloatTensor<B>>,
571        options: ConvTransposeOptions<2>,
572    ) -> FloatTensor<B>;
573    /// Backward pass for the [conv transpose 2d](ModuleOps::conv_transpose2d) operation, returning the gradient for `x`.
574    fn conv_transpose2d_x_backward(
575        weight: FloatTensor<B>,
576        output_grad: FloatTensor<B>,
577        options: ConvTransposeOptions<2>,
578    ) -> FloatTensor<B> {
579        conv::conv_transpose2d_x_backward::<B>(weight, output_grad, options)
580    }
581    /// Backward pass for the [conv transpose 2d](ModuleOps::conv_transpose2d) operation, returning the gradient for `weight`.
582    fn conv_transpose2d_weight_backward(
583        x: FloatTensor<B>,
584        weight: FloatTensor<B>,
585        output_grad: FloatTensor<B>,
586        options: ConvTransposeOptions<2>,
587    ) -> FloatTensor<B> {
588        conv::conv_transpose2d_weight_backward::<B>(x, weight, output_grad, options)
589    }
590    /// Backward pass for the [conv transpose 2d](ModuleOps::conv_transpose2d) operation, returning the gradient for `bias`.
591    fn conv_transpose2d_bias_backward(
592        x: FloatTensor<B>,
593        bias: FloatTensor<B>,
594        output_grad: FloatTensor<B>,
595    ) -> FloatTensor<B> {
596        conv::conv_transpose2d_bias_backward::<B>(x, bias, output_grad)
597    }
598
599    /// Three dimensional transposed convolution.
600    ///
601    /// # Shapes
602    ///
603    /// x:      `[batch_size, channels_in, height, width]`,
604    /// weight: `[channels_in, channels_out, kernel_size_1, kernel_size_2, kernel_size_3]`,
605    /// bias:   `[channels_out]`,
606    fn conv_transpose3d(
607        x: FloatTensor<B>,
608        weight: FloatTensor<B>,
609        bias: Option<FloatTensor<B>>,
610        options: ConvTransposeOptions<3>,
611    ) -> FloatTensor<B>;
612    /// Backward pass for the [conv transpose 3d](ModuleOps::conv_transpose3d) operation, returning the gradient for `x`.
613    fn conv_transpose3d_x_backward(
614        weight: FloatTensor<B>,
615        output_grad: FloatTensor<B>,
616        options: ConvTransposeOptions<3>,
617    ) -> FloatTensor<B> {
618        conv::conv_transpose3d_x_backward::<B>(weight, output_grad, options)
619    }
620    /// Backward pass for the [conv transpose 3d](ModuleOps::conv_transpose3d) operation, returning the gradient for `weight`.
621    fn conv_transpose3d_weight_backward(
622        x: FloatTensor<B>,
623        weight: FloatTensor<B>,
624        output_grad: FloatTensor<B>,
625        options: ConvTransposeOptions<3>,
626    ) -> FloatTensor<B> {
627        conv::conv_transpose3d_weight_backward::<B>(x, weight, output_grad, options)
628    }
629    /// Backward pass for the [conv transpose 3d](ModuleOps::conv_transpose3d) operation, returning the gradient for `bias`.
630    fn conv_transpose3d_bias_backward(
631        x: FloatTensor<B>,
632        bias: FloatTensor<B>,
633        output_grad: FloatTensor<B>,
634    ) -> FloatTensor<B> {
635        conv::conv_transpose3d_bias_backward::<B>(x, bias, output_grad)
636    }
637
638    /// Four-dimensional unfolding.
639    ///
640    /// # Shapes
641    ///
642    /// * x:      ``[batch_size, channels_in, height, width]``,
643    /// * returns: ``[batch_size, channels_in * kernel_size_1 * kernel_size_2, number of blocks]``,
644    fn unfold4d(
645        x: FloatTensor<B>,
646        kernel_size: [usize; 2],
647        options: UnfoldOptions,
648    ) -> FloatTensor<B> {
649        if options.padding == [0, 0] && options.dilation == [1, 1] {
650            let blocks = B::float_unfold(x, 2, kernel_size[0], options.stride[0]);
651            let blocks = B::float_unfold(blocks, 3, kernel_size[1], options.stride[1]);
652
653            // batch, channels, h_blocks, w_blocks, h_kern, w_kern
654
655            let blocks = B::float_permute(blocks, &[0, 1, 4, 5, 2, 3]);
656            let shape = blocks.shape();
657
658            // batch, channels, h_kern, w_kern, h_blocks, w_blocks
659
660            B::float_reshape(
661                blocks,
662                [
663                    shape[0],
664                    shape[1] * shape[2] * shape[3],
665                    shape[4] * shape[5],
666                ]
667                .into(),
668            )
669        } else {
670            unfold4d_using_conv2d::<B>(x, kernel_size, options)
671        }
672    }
673
674    /// Four dimensional fold (`col2im`), the adjoint of [unfold4d](ModuleOps::unfold4d).
675    ///
676    /// Composes [conv_transpose2d](ModuleOps::conv_transpose2d) with the same one-hot channel mapping
677    /// [unfold4d](ModuleOps::unfold4d) uses, so backends inherit a correct (and differentiable)
678    /// implementation for free and may override it with a custom one.
679    ///
680    /// # Shapes
681    ///
682    /// x: `[batch_size, channels * kernel_size_0 * kernel_size_1, num_blocks]`,
683    /// output: `[batch_size, channels, output_size_0, output_size_1]`
684    fn fold4d(
685        x: FloatTensor<B>,
686        output_size: [usize; 2],
687        kernel_size: [usize; 2],
688        options: UnfoldOptions,
689    ) -> FloatTensor<B> {
690        let [batch_size, channels_col, num_blocks] = x.shape().dims();
691        let [kernel_height, kernel_width] = kernel_size;
692        let [output_height, output_width] = output_size;
693        let [stride_height, stride_width] = options.stride;
694        let [padding_height, padding_width] = options.padding;
695        let [dilation_height, dilation_width] = options.dilation;
696
697        let kernel_elems = kernel_height * kernel_width;
698        assert_eq!(
699            channels_col % kernel_elems,
700            0,
701            "fold4d: input channels ({channels_col}) must be divisible by the kernel size product ({kernel_elems})"
702        );
703        let channels = channels_col / kernel_elems;
704
705        // Number of sliding blocks along each spatial dimension (the unfold output grid).
706        let blocks_height =
707            (output_height + 2 * padding_height - dilation_height * (kernel_height - 1) - 1)
708                / stride_height
709                + 1;
710        let blocks_width =
711            (output_width + 2 * padding_width - dilation_width * (kernel_width - 1) - 1)
712                / stride_width
713                + 1;
714        assert_eq!(
715            num_blocks,
716            blocks_height * blocks_width,
717            "fold4d: number of blocks ({num_blocks}) does not match the expected grid ({blocks_height} x {blocks_width}) for the given output size and options"
718        );
719
720        // Fold is the adjoint of unfold and uses the same one-hot channel mapping. Grouping by
721        // channel removes the otherwise unused cross-channel weights.
722        let groups = channels.max(1);
723        let weight =
724            create_unfolding_weight::<B>(channels, kernel_size, groups, &x.device(), x.dtype());
725
726        // Reshape the columns into the spatial grid of blocks, then scatter-add them back.
727        let x = B::float_reshape(
728            x,
729            Shape::new([batch_size, channels_col, blocks_height, blocks_width]),
730        );
731
732        // `padding_out` recovers the exact requested output size (always `< stride`).
733        let padding_out = [
734            (output_height + 2 * padding_height - dilation_height * (kernel_height - 1) - 1)
735                % stride_height,
736            (output_width + 2 * padding_width - dilation_width * (kernel_width - 1) - 1)
737                % stride_width,
738        ];
739
740        B::conv_transpose2d(
741            x,
742            weight,
743            None,
744            ConvTransposeOptions::new(
745                options.stride,
746                options.padding,
747                padding_out,
748                options.dilation,
749                groups,
750            ),
751        )
752    }
753
754    /// One dimensional avg pooling.
755    ///
756    /// # Shapes
757    ///
758    /// x: [batch_size, channels, length],
759    fn avg_pool1d(
760        x: FloatTensor<B>,
761        kernel_size: usize,
762        stride: usize,
763        padding: usize,
764        dilation: usize,
765        count_include_pad: bool,
766        ceil_mode: bool,
767    ) -> FloatTensor<B> {
768        pool::avg_pool1d_from_2d::<B>(
769            x,
770            kernel_size,
771            stride,
772            padding,
773            dilation,
774            count_include_pad,
775            ceil_mode,
776        )
777    }
778    /// Backward pass for the [avg pooling 1d](ModuleOps::avg_pool1d) operation.
779    #[allow(clippy::too_many_arguments)]
780    fn avg_pool1d_backward(
781        x: FloatTensor<B>,
782        grad: FloatTensor<B>,
783        kernel_size: usize,
784        stride: usize,
785        padding: usize,
786        dilation: usize,
787        count_include_pad: bool,
788        ceil_mode: bool,
789    ) -> FloatTensor<B> {
790        pool::avg_pool1d_backward_from_2d::<B>(
791            x,
792            grad,
793            kernel_size,
794            stride,
795            padding,
796            dilation,
797            count_include_pad,
798            ceil_mode,
799        )
800    }
801    /// Two dimensional avg pooling.
802    ///
803    /// # Shapes
804    ///
805    /// x: [batch_size, channels, height, width],
806    fn avg_pool2d(
807        x: FloatTensor<B>,
808        kernel_size: [usize; 2],
809        stride: [usize; 2],
810        padding: [usize; 2],
811        dilation: [usize; 2],
812        count_include_pad: bool,
813        ceil_mode: bool,
814    ) -> FloatTensor<B>;
815    /// Backward pass for the [avg pooling 2d](ModuleOps::avg_pool2d) operation.
816    #[allow(clippy::too_many_arguments)]
817    fn avg_pool2d_backward(
818        x: FloatTensor<B>,
819        grad: FloatTensor<B>,
820        kernel_size: [usize; 2],
821        stride: [usize; 2],
822        padding: [usize; 2],
823        dilation: [usize; 2],
824        count_include_pad: bool,
825        ceil_mode: bool,
826    ) -> FloatTensor<B>;
827    /// Three dimensional avg pooling.
828    ///
829    /// # Shapes
830    ///
831    /// x: [batch_size, channels, depth, height, width],
832    fn avg_pool3d(
833        x: FloatTensor<B>,
834        kernel_size: [usize; 3],
835        stride: [usize; 3],
836        padding: [usize; 3],
837        dilation: [usize; 3],
838        count_include_pad: bool,
839        ceil_mode: bool,
840    ) -> FloatTensor<B>;
841    /// Backward pass for the [avg pooling 3d](ModuleOps::avg_pool3d) operation.
842    #[allow(clippy::too_many_arguments)]
843    fn avg_pool3d_backward(
844        x: FloatTensor<B>,
845        grad: FloatTensor<B>,
846        kernel_size: [usize; 3],
847        stride: [usize; 3],
848        padding: [usize; 3],
849        dilation: [usize; 3],
850        count_include_pad: bool,
851        ceil_mode: bool,
852    ) -> FloatTensor<B>;
853    /// Two dimensional adaptive avg pooling.
854    ///
855    /// # Shapes
856    ///
857    /// x: [batch_size, channels, height, width],
858    fn adaptive_avg_pool2d(x: FloatTensor<B>, output_size: [usize; 2]) -> FloatTensor<B>;
859    /// Backward pass for the [adaptive avg pooling 2d](ModuleOps::adaptive_avg_pool2d) operation.
860    fn adaptive_avg_pool2d_backward(x: FloatTensor<B>, grad: FloatTensor<B>) -> FloatTensor<B>;
861    /// Three dimensional adaptive avg pooling.
862    ///
863    /// # Shapes
864    ///
865    /// x: [batch_size, channels, depth, height, width],
866    fn adaptive_avg_pool3d(x: FloatTensor<B>, output_size: [usize; 3]) -> FloatTensor<B>;
867    /// Backward pass for the [adaptive avg pooling 3d](ModuleOps::adaptive_avg_pool3d) operation.
868    fn adaptive_avg_pool3d_backward(x: FloatTensor<B>, grad: FloatTensor<B>) -> FloatTensor<B>;
869    /// One dimensional adaptive avg pooling.
870    ///
871    /// # Shapes
872    ///
873    /// x: [batch_size, channels, length],
874    fn adaptive_avg_pool1d(x: FloatTensor<B>, output_size: usize) -> FloatTensor<B> {
875        pool::adaptive_avg_pool1d_from_2d::<B>(x, output_size)
876    }
877    /// Backward pass for the [adaptive avg pooling 1d](ModuleOps::adaptive_avg_pool1d) operation.
878    fn adaptive_avg_pool1d_backward(x: FloatTensor<B>, grad: FloatTensor<B>) -> FloatTensor<B> {
879        pool::adaptive_avg_pool1d_backward_from_2d::<B>(x, grad)
880    }
881    /// One dimensional max pooling.
882    ///
883    /// # Shapes
884    ///
885    /// x: [batch_size, channels, length],
886    fn max_pool1d(
887        x: FloatTensor<B>,
888        kernel_size: usize,
889        stride: usize,
890        padding: usize,
891        dilation: usize,
892        ceil_mode: bool,
893    ) -> FloatTensor<B> {
894        pool::max_pool1d_from_2d::<B>(x, kernel_size, stride, padding, dilation, ceil_mode)
895    }
896
897    /// One dimensional max pooling with indices.
898    ///
899    /// # Shapes
900    ///
901    /// x: [batch_size, channels, height, width],
902    fn max_pool1d_with_indices(
903        x: FloatTensor<B>,
904        kernel_size: usize,
905        stride: usize,
906        padding: usize,
907        dilation: usize,
908        ceil_mode: bool,
909        indices_dtype: IntDType,
910    ) -> MaxPool1dWithIndices<B> {
911        pool::max_pool1d_with_indices_from_2d::<B>(
912            x,
913            kernel_size,
914            stride,
915            padding,
916            dilation,
917            ceil_mode,
918            indices_dtype,
919        )
920    }
921    /// Backward pass for the [max pooling 1d](ModuleOps::max_pool1d_with_indices) operation.
922    #[allow(clippy::too_many_arguments)]
923    fn max_pool1d_with_indices_backward(
924        x: FloatTensor<B>,
925        kernel_size: usize,
926        stride: usize,
927        padding: usize,
928        dilation: usize,
929        ceil_mode: bool,
930        output_grad: FloatTensor<B>,
931        indices: IntTensor<B>,
932    ) -> MaxPool1dBackward<B> {
933        pool::max_pool1d_with_indices_backward_from_2d::<B>(
934            x,
935            kernel_size,
936            stride,
937            padding,
938            dilation,
939            ceil_mode,
940            output_grad,
941            indices,
942        )
943    }
944
945    /// Two dimensional max pooling.
946    ///
947    /// # Shapes
948    ///
949    /// x: [batch_size, channels, height, width],
950    fn max_pool2d(
951        x: FloatTensor<B>,
952        kernel_size: [usize; 2],
953        stride: [usize; 2],
954        padding: [usize; 2],
955        dilation: [usize; 2],
956        ceil_mode: bool,
957    ) -> FloatTensor<B>;
958
959    /// Two dimensional max pooling with indices.
960    ///
961    /// # Shapes
962    ///
963    /// x: [batch_size, channels, height, width],
964    fn max_pool2d_with_indices(
965        x: FloatTensor<B>,
966        kernel_size: [usize; 2],
967        stride: [usize; 2],
968        padding: [usize; 2],
969        dilation: [usize; 2],
970        ceil_mode: bool,
971        indices_dtype: IntDType,
972    ) -> MaxPool2dWithIndices<B>;
973    /// Backward pass for the [max pooling 2d](ModuleOps::max_pool2d_with_indices) operation.
974    #[allow(clippy::too_many_arguments)]
975    fn max_pool2d_with_indices_backward(
976        x: FloatTensor<B>,
977        kernel_size: [usize; 2],
978        stride: [usize; 2],
979        padding: [usize; 2],
980        dilation: [usize; 2],
981        ceil_mode: bool,
982        output_grad: FloatTensor<B>,
983        indices: IntTensor<B>,
984    ) -> MaxPool2dBackward<B>;
985
986    /// Three dimensional max pooling.
987    ///
988    /// # Shapes
989    ///
990    /// x: [batch_size, channels, depth, height, width],
991    fn max_pool3d(
992        x: FloatTensor<B>,
993        kernel_size: [usize; 3],
994        stride: [usize; 3],
995        padding: [usize; 3],
996        dilation: [usize; 3],
997        ceil_mode: bool,
998    ) -> FloatTensor<B>;
999
1000    /// Three dimensional max pooling with indices.
1001    ///
1002    /// # Shapes
1003    ///
1004    /// x: [batch_size, channels, depth, height, width],
1005    fn max_pool3d_with_indices(
1006        x: FloatTensor<B>,
1007        kernel_size: [usize; 3],
1008        stride: [usize; 3],
1009        padding: [usize; 3],
1010        dilation: [usize; 3],
1011        ceil_mode: bool,
1012        indices_dtype: IntDType,
1013    ) -> MaxPool3dWithIndices<B>;
1014    /// Backward pass for the [max pooling 3d](ModuleOps::max_pool3d_with_indices) operation.
1015    #[allow(clippy::too_many_arguments)]
1016    fn max_pool3d_with_indices_backward(
1017        x: FloatTensor<B>,
1018        kernel_size: [usize; 3],
1019        stride: [usize; 3],
1020        padding: [usize; 3],
1021        dilation: [usize; 3],
1022        ceil_mode: bool,
1023        output_grad: FloatTensor<B>,
1024        indices: IntTensor<B>,
1025    ) -> MaxPool3dBackward<B>;
1026
1027    /// Down/up samples the input.
1028    ///
1029    /// # Shapes
1030    ///
1031    /// x: `[batch_size, channels, height, width]`,
1032    fn interpolate(
1033        x: FloatTensor<B>,
1034        output_size: [usize; 2],
1035        options: InterpolateOptions,
1036    ) -> FloatTensor<B>;
1037
1038    /// Backward pass for the [interpolate](ModuleOps::interpolate) operation.
1039    fn interpolate_backward(
1040        x: FloatTensor<B>,
1041        grad: FloatTensor<B>,
1042        output_size: [usize; 2],
1043        options: InterpolateOptions,
1044    ) -> FloatTensor<B>;
1045
1046    /// Computes scaled dot-product attention: softmax(QKᵗ * scale) · V,
1047    /// where scale defaults to 1/sqrt(head_dim). Optionally applies masking,
1048    /// additive bias, causal masking, and softcap to the attention scores.
1049    ///
1050    /// The scores are computed in this order, which every backend must follow:
1051    /// 1. `s = Q·Kᵗ · scale`
1052    /// 2. `s = softcap · tanh(s / softcap)` if `options.softcap` is set
1053    /// 3. `s = -inf` where `mask` is `true` or the causal mask hides the key
1054    /// 4. `s = s + attn_bias`
1055    /// 5. `p = softmax(s)` over keys; a row with every key hidden yields `p = 0`
1056    /// 6. `out = p · V`
1057    ///
1058    /// Softcap comes before the masks so that hidden keys stay at `-inf`.
1059    ///
1060    /// # Arguments
1061    /// - `query`: Query tensor of shape `[batch_size, num_heads, seq_len_q, head_dim]`
1062    /// - `key`: Key tensor of shape `[batch_size, num_kv_heads, seq_len_k, head_dim]`
1063    /// - `value`: Value tensor of shape `[batch_size, num_kv_heads, seq_len_k, val_dim]`
1064    ///
1065    ///   `num_heads` must be a multiple of `num_kv_heads` (grouped-query attention;
1066    ///   `num_kv_heads == 1` is multi-query attention). Query head `h` attends with K/V
1067    ///   head `h / (num_heads / num_kv_heads)`, the PyTorch and ONNX convention.
1068    /// - `mask`: Optional boolean mask of shape `[batch_size, num_heads, seq_len_q, seq_len_k]`,
1069    ///   where `true` indicates positions to mask (i.e. set to -inf before softmax).
1070    /// - `attn_bias`: Optional float tensor of shape `[batch_size, num_heads, seq_len_q, seq_len_k]`
1071    ///   added to the attention scores before softmax (e.g. ALiBi, relative position biases).
1072    /// - `options`: Additional attention options (custom scale, softcap, causal masking
1073    ///   and its [alignment](crate::ops::CausalAlignment)).
1074    ///
1075    /// # Returns
1076    /// A tensor of shape `[batch_size, num_heads, seq_len_q, val_dim]`
1077    /// representing the attended context per head.
1078    ///
1079    /// # Note
1080    /// This implementation does not support dropout and is intended for inference or
1081    /// use cases where dropout is not needed.
1082    fn attention(
1083        query: FloatTensor<B>,
1084        key: FloatTensor<B>,
1085        value: FloatTensor<B>,
1086        mask: Option<BoolTensor<B>>,
1087        attn_bias: Option<FloatTensor<B>>,
1088        options: AttentionModuleOptions,
1089    ) -> FloatTensor<B>;
1090
1091    /// Returns `true` if this backend implements
1092    /// [attention_backward](ModuleOps::attention_backward) natively.
1093    ///
1094    /// Autodiff queries this flag, for an attention with no mask and no additive bias, to decide
1095    /// between two paths:
1096    /// - `true`: the forward pass is [attention_with_logsumexp](ModuleOps::attention_with_logsumexp)
1097    ///   and the backward pass `attention_backward`, which reads the probabilities back from the
1098    ///   logsumexp instead of keeping the scores.
1099    /// - `false`: autodiff differentiates the decomposed
1100    ///   [attention_fallback](attention::attention_fallback).
1101    ///
1102    /// Backends that override `attention_backward` must also override this to return `true`.
1103    fn has_attention_backward() -> bool {
1104        false
1105    }
1106
1107    /// [attention](ModuleOps::attention) with no mask and no additive bias, and each query's
1108    /// logsumexp of its scores, `[batch_size, num_heads, seq_len_q]` in `f32` whatever the
1109    /// operands' dtype: the forward pass [attention_backward](ModuleOps::attention_backward) reads
1110    /// back, where `exp(score − logsumexp)` is where its precision is lost.
1111    ///
1112    /// The default composes it from tensor operations
1113    /// ([attention::attention_with_logsumexp_default]).
1114    fn attention_with_logsumexp(
1115        query: FloatTensor<B>,
1116        key: FloatTensor<B>,
1117        value: FloatTensor<B>,
1118        options: AttentionModuleOptions,
1119    ) -> (FloatTensor<B>, FloatTensor<B>) {
1120        attention::attention_with_logsumexp_default::<B>(query, key, value, options)
1121    }
1122
1123    /// Backward pass for [attention_with_logsumexp](ModuleOps::attention_with_logsumexp): the
1124    /// gradients against the query, the key and the value, from `out_grad`, the loss's gradient
1125    /// against the output, and the forward's `out` and `logsumexp`.
1126    ///
1127    /// Autodiff only calls it when [has_attention_backward](ModuleOps::has_attention_backward)
1128    /// returns `true`. The default composes the gradients from tensor operations
1129    /// ([attention::attention_backward_default]).
1130    fn attention_backward(
1131        query: FloatTensor<B>,
1132        key: FloatTensor<B>,
1133        value: FloatTensor<B>,
1134        out: FloatTensor<B>,
1135        logsumexp: FloatTensor<B>,
1136        out_grad: FloatTensor<B>,
1137        options: AttentionModuleOptions,
1138    ) -> AttentionBackward<B> {
1139        attention::attention_backward_default::<B>(
1140            query, key, value, out, logsumexp, out_grad, options,
1141        )
1142    }
1143
1144    /// Applies Layer Normalization over the last dimension of the input tensor.
1145    ///
1146    /// Computes `(x - mean) / sqrt(var + epsilon) * gamma + beta`, where `mean` and
1147    /// (biased) `var` are reduced over the last axis.
1148    ///
1149    /// # Arguments
1150    ///
1151    /// * `tensor` - Input tensor of shape `[..., d_model]`.
1152    /// * `gamma` - Scale tensor of shape `[d_model]`.
1153    /// * `beta` - Optional bias tensor of shape `[d_model]`.
1154    /// * `epsilon` - Numerical stability term added to the variance before the square root.
1155    ///
1156    /// # Returns
1157    ///
1158    /// A tensor with the same shape as `tensor`.
1159    fn layer_norm(
1160        tensor: FloatTensor<B>,
1161        gamma: FloatTensor<B>,
1162        beta: Option<FloatTensor<B>>,
1163        epsilon: f64,
1164    ) -> FloatTensor<B> {
1165        let shape = tensor.shape();
1166        let rank = shape.num_dims();
1167        let last_dim = rank - 1;
1168        let d_model = shape[last_dim];
1169
1170        let mean = B::float_mean_dim(tensor.clone(), last_dim);
1171        let centered = B::float_sub(tensor, mean);
1172        let var = B::float_mean_dim(B::float_mul(centered.clone(), centered.clone()), last_dim);
1173        let denom = B::float_sqrt(B::float_add_scalar(var, epsilon.into()));
1174        let normalized = B::float_div(centered, denom);
1175
1176        let broadcast_dims: alloc::vec::Vec<usize> = (0..rank)
1177            .map(|i| if i == last_dim { d_model } else { 1 })
1178            .collect();
1179        let gamma_b = B::float_reshape(gamma, Shape::from(broadcast_dims.clone()));
1180        let scaled = B::float_mul(normalized, gamma_b);
1181
1182        match beta {
1183            Some(beta) => {
1184                let beta_b = B::float_reshape(beta, Shape::from(broadcast_dims));
1185                B::float_add(scaled, beta_b)
1186            }
1187            None => scaled,
1188        }
1189    }
1190
1191    /// Computes the Connectionist Temporal Classification (CTC) loss.
1192    ///
1193    /// Sums over all valid alignments between the input and target sequences
1194    /// using the forward (alpha) algorithm.
1195    ///
1196    /// # Arguments
1197    ///
1198    /// * `log_probs` - Log-probabilities of shape `[T, N, C]`
1199    /// * `targets` - Target label indices of shape `[N, S]`
1200    /// * `input_lengths` - Actual input sequence lengths per batch element `[N]`
1201    /// * `target_lengths` - Actual target lengths per batch element `[N]`
1202    /// * `blank` - Index of the blank label
1203    ///
1204    /// # Returns
1205    ///
1206    /// Per-sample loss of shape `[N]`
1207    fn ctc_loss(
1208        log_probs: FloatTensor<B>,
1209        targets: IntTensor<B>,
1210        input_lengths: IntTensor<B>,
1211        target_lengths: IntTensor<B>,
1212        blank: usize,
1213    ) -> FloatTensor<B> {
1214        ctc::ctc_loss_default::<B>(log_probs, targets, input_lengths, target_lengths, blank)
1215    }
1216
1217    /// Returns `true` if this backend implements [ctc_loss_backward](ModuleOps::ctc_loss_backward)
1218    /// natively.
1219    ///
1220    /// Autodiff queries this flag to decide between two paths:
1221    /// - `true`: use the backend's [ctc_loss](ModuleOps::ctc_loss) and
1222    ///   [ctc_loss_backward](ModuleOps::ctc_loss_backward) directly.
1223    /// - `false`: call [ctc::ctc_loss_default] for the forward pass; autodiff
1224    ///   then differentiates through the decomposed tensor ops.
1225    ///
1226    /// Backends that override `ctc_loss_backward` must also override this to
1227    /// return `true`.
1228    fn has_ctc_loss_backward() -> bool {
1229        false
1230    }
1231
1232    /// Backward pass for [ctc_loss](ModuleOps::ctc_loss): gradient w.r.t. `log_probs`.
1233    ///
1234    /// Autodiff only calls it when [has_ctc_loss_backward](ModuleOps::has_ctc_loss_backward)
1235    /// returns `true`. Backends without a native implementation should leave
1236    /// both methods at their defaults; the gradient is computed automatically by
1237    /// autodiff against the decomposed [ctc::ctc_loss_default] forward.
1238    ///
1239    /// The default composes the gradient from tensor operations
1240    /// ([ctc::ctc_loss_backward_default]). It serves callers that hold no autodiff
1241    /// graph for this backend, such as the interpreter behind a router, whose
1242    /// client asked for the gradient as one operation.
1243    ///
1244    /// # Arguments
1245    ///
1246    /// * `log_probs` - Log-probabilities of shape `[T, N, C]`
1247    /// * `targets` - Target label indices of shape `[N, S]`
1248    /// * `input_lengths` - Actual input sequence lengths per batch element `[N]`
1249    /// * `target_lengths` - Actual target lengths per batch element `[N]`
1250    /// * `grad_loss` - Upstream gradient w.r.t. the per-sample loss `[N]`
1251    /// * `blank` - Index of the blank label
1252    ///
1253    /// # Returns
1254    ///
1255    /// Gradient w.r.t. `log_probs` of shape `[T, N, C]`
1256    fn ctc_loss_backward(
1257        log_probs: FloatTensor<B>,
1258        targets: IntTensor<B>,
1259        input_lengths: IntTensor<B>,
1260        target_lengths: IntTensor<B>,
1261        grad_loss: FloatTensor<B>,
1262        blank: usize,
1263    ) -> FloatTensor<B> {
1264        ctc::ctc_loss_backward_default::<B>(
1265            log_probs,
1266            targets,
1267            input_lengths,
1268            target_lengths,
1269            grad_loss,
1270            blank,
1271        )
1272    }
1273}