Skip to main content

burn_tensor/tensor/
module.rs

1use burn_backend::ops::ModuleOps;
2use burn_dispatch::Dispatch;
3use burn_std::{MatmulTransformAction, MatmulTransformAnalysis, MatmulTransformPolicy};
4
5use crate::{
6    Bool, DType, Int, Tensor, check,
7    check::TensorCheck,
8    kind::Basic,
9    ops::{
10        AttentionModuleOptions, AvgPoolOptions, BridgeTensor, ConvOptions, ConvTransposeOptions,
11        DeformConvOptions, InterpolateOptions, MaxPoolOptions, PadMode, UnfoldOptions,
12    },
13};
14
15/// Applies batch normalization using explicitly supplied channel statistics.
16///
17/// `input` has shape `[batch, channels, ...]`; `gamma`, `beta`, `mean`, and
18/// `variance` each have shape `[channels]`.
19///
20/// This function doesn't calculate or update statistics. Callers may supply
21/// running statistics for inference or batch statistics calculated by a
22/// training path.
23pub fn batch_norm<const D: usize>(
24    input: Tensor<D>,
25    gamma: Tensor<1>,
26    beta: Tensor<1>,
27    mean: Tensor<1>,
28    variance: Tensor<1>,
29    epsilon: f64,
30) -> Tensor<D> {
31    assert!(D >= 2, "batch norm requires an input rank of at least 2");
32    let channels = input.dims()[1];
33    assert_eq!(gamma.dims(), [channels], "invalid batch norm gamma shape");
34    assert_eq!(beta.dims(), [channels], "invalid batch norm beta shape");
35    assert_eq!(mean.dims(), [channels], "invalid batch norm mean shape");
36    assert_eq!(
37        variance.dims(),
38        [channels],
39        "invalid batch norm variance shape"
40    );
41    Tensor::new(BridgeTensor::float(Dispatch::batch_norm(
42        input.primitive.into_float(),
43        gamma.primitive.into_float(),
44        beta.primitive.into_float(),
45        mean.primitive.into_float(),
46        variance.primitive.into_float(),
47        epsilon,
48    )))
49}
50
51/// Output and batch statistics from [`batch_norm_train`].
52pub struct BatchNormTrainOutput<const D: usize> {
53    /// The normalized input, with the same shape as the input.
54    pub output: Tensor<D>,
55
56    /// The batch mean with shape `[channels]`, detached on autodiff devices.
57    pub mean: Tensor<1>,
58
59    /// The biased (population) batch variance with shape `[channels]`, excluding
60    /// epsilon and detached on autodiff devices.
61    pub variance: Tensor<1>,
62}
63
64/// Applies batch normalization using statistics computed from the input batch.
65///
66/// `input` has shape `[batch, channels, ...]`; `gamma` and `beta` have shape
67/// `[channels]`.
68///
69/// Returns the normalized input with its original shape, along with the batch
70/// mean and biased (population) variance, both with shape `[channels]`.
71/// Statistics are computed over every dimension except the channel dimension.
72/// The returned variance excludes `epsilon`.
73///
74/// This function does not update running statistics. For normalization using
75/// explicitly supplied statistics, use [`batch_norm`].
76///
77/// # Autodiff
78///
79/// This function can be used with or without autodiff. It does not enable
80/// gradient tracking.
81///
82/// On an autodiff-enabled device, gradients through the normalized output
83/// account for the dependence of the batch statistics on the input. The returned
84/// mean and variance remain on the same device but are detached.
85pub fn batch_norm_train<const D: usize>(
86    input: Tensor<D>,
87    gamma: Tensor<1>,
88    beta: Tensor<1>,
89    epsilon: f64,
90) -> BatchNormTrainOutput<D> {
91    assert!(D >= 2, "batch norm requires an input rank of at least 2");
92    let channels = input.dims()[1];
93    assert_eq!(gamma.dims(), [channels], "invalid batch norm gamma shape");
94    assert_eq!(beta.dims(), [channels], "invalid batch norm beta shape");
95    let result = Dispatch::batch_norm_train(
96        input.primitive.into_float(),
97        gamma.primitive.into_float(),
98        beta.primitive.into_float(),
99        epsilon,
100    );
101
102    BatchNormTrainOutput {
103        output: Tensor::new(BridgeTensor::float(result.output)),
104        mean: Tensor::new(BridgeTensor::float(result.mean)),
105        variance: Tensor::new(BridgeTensor::float(result.variance)),
106    }
107}
108
109/// Computes the [CTC loss](burn_backend::ops::ModuleOps::ctc_loss).
110///
111/// # Arguments
112///
113/// * `log_probs` - Log-probabilities of shape `[T, N, C]`
114/// * `targets` - Target label indices of shape `[N, S]`
115/// * `input_lengths` - Actual input sequence lengths per batch element `[N]`
116/// * `target_lengths` - Actual target lengths per batch element `[N]`
117/// * `blank` - Index of the blank label
118///
119/// # Returns
120///
121/// Per-sample loss of shape `[N]`
122pub fn ctc_loss(
123    log_probs: Tensor<3>,
124    targets: Tensor<2, Int>,
125    input_lengths: Tensor<1, Int>,
126    target_lengths: Tensor<1, Int>,
127    blank: usize,
128) -> Tensor<1> {
129    Tensor::new(BridgeTensor::float(Dispatch::ctc_loss(
130        log_probs.primitive.into_float(),
131        targets.primitive.into(),
132        input_lengths.primitive.into(),
133        target_lengths.primitive.into(),
134        blank,
135    )))
136}
137
138/// Applies the [embedding module](burn_backend::ops::ModuleOps::embedding).
139pub fn embedding(weights: Tensor<2>, indices: Tensor<2, Int>) -> Tensor<3> {
140    Tensor::new(BridgeTensor::float(Dispatch::embedding(
141        weights.primitive.into_float(),
142        indices.primitive.into(),
143    )))
144}
145
146/// Applies a [1D convolution](burn_backend::ops::ModuleOps::conv1d).
147///
148/// Supports symmetric and asymmetric padding through [`ConvOptions`].
149/// The deprecated [`PaddedConvOptions`](crate::ops::PaddedConvOptions) is also
150/// accepted for compatibility.
151pub fn conv1d(
152    x: Tensor<3>,
153    weight: Tensor<3>,
154    bias: Option<Tensor<1>>,
155    options: impl Into<ConvOptions<1>>,
156) -> Tensor<3> {
157    let options = options.into();
158    check!(TensorCheck::conv(
159        "conv1d",
160        x.dims(),
161        weight.dims(),
162        options.groups,
163    ));
164
165    Tensor::new(BridgeTensor::float(Dispatch::conv1d(
166        x.primitive.into_float(),
167        weight.primitive.into_float(),
168        bias.map(|b| b.primitive.into_float()),
169        options,
170    )))
171}
172
173/// Applies a [2D convolution](burn_backend::ops::ModuleOps::conv2d).
174///
175/// Supports symmetric and asymmetric padding through [`ConvOptions`].
176/// The deprecated [`PaddedConvOptions`](crate::ops::PaddedConvOptions) is also
177/// accepted for compatibility.
178pub fn conv2d(
179    x: Tensor<4>,
180    weight: Tensor<4>,
181    bias: Option<Tensor<1>>,
182    options: impl Into<ConvOptions<2>>,
183) -> Tensor<4> {
184    let options = options.into();
185    check!(TensorCheck::conv(
186        "conv2d",
187        x.dims(),
188        weight.dims(),
189        options.groups,
190    ));
191
192    Tensor::new(BridgeTensor::float(Dispatch::conv2d(
193        x.primitive.into_float(),
194        weight.primitive.into_float(),
195        bias.map(|b| b.primitive.into_float()),
196        options,
197    )))
198}
199
200/// Applies a [3D convolution](burn_backend::ops::ModuleOps::conv3d).
201///
202/// Asymmetric 3D padding is not yet supported.
203/// The deprecated [`PaddedConvOptions`](crate::ops::PaddedConvOptions) is also
204/// accepted for compatibility.
205pub fn conv3d(
206    x: Tensor<5>,
207    weight: Tensor<5>,
208    bias: Option<Tensor<1>>,
209    options: impl Into<ConvOptions<3>>,
210) -> Tensor<5> {
211    let options = options.into();
212    check!(TensorCheck::conv(
213        "conv3d",
214        x.dims(),
215        weight.dims(),
216        options.groups,
217    ));
218
219    if options.is_asymmetric() {
220        panic!("Asymmetric padding is not yet supported for conv3d");
221    }
222
223    Tensor::new(BridgeTensor::float(Dispatch::conv3d(
224        x.primitive.into_float(),
225        weight.primitive.into_float(),
226        bias.map(|b| b.primitive.into_float()),
227        options,
228    )))
229}
230
231/// Applies a [Deformable 2D convolution](burn_backend::ops::ModuleOps::deform_conv2d).
232pub fn deform_conv2d(
233    x: Tensor<4>,
234    offset: Tensor<4>,
235    weight: Tensor<4>,
236    mask: Option<Tensor<4>>,
237    bias: Option<Tensor<1>>,
238    options: DeformConvOptions<2>,
239) -> Tensor<4> {
240    check!(TensorCheck::conv(
241        "deform_conv2d",
242        x.dims(),
243        weight.dims(),
244        options.weight_groups,
245    ));
246    Tensor::new(BridgeTensor::float(Dispatch::deform_conv2d(
247        x.primitive.into_float(),
248        offset.primitive.into_float(),
249        weight.primitive.into_float(),
250        mask.map(|m| m.primitive.into_float()),
251        bias.map(|b| b.primitive.into_float()),
252        options,
253    )))
254}
255
256/// Applies a [1D transposed convolution](burn_backend::ops::ModuleOps::conv_transpose1d).
257pub fn conv_transpose1d(
258    x: Tensor<3>,
259    weight: Tensor<3>,
260    bias: Option<Tensor<1>>,
261    options: ConvTransposeOptions<1>,
262) -> Tensor<3> {
263    check!(TensorCheck::conv_transpose(
264        "conv_transpose1d",
265        x.dims(),
266        weight.dims(),
267    ));
268    Tensor::new(BridgeTensor::float(Dispatch::conv_transpose1d(
269        x.primitive.into_float(),
270        weight.primitive.into_float(),
271        bias.map(|b| b.primitive.into_float()),
272        options,
273    )))
274}
275
276/// Applies a [2D transposed convolution](burn_backend::ops::ModuleOps::conv_transpose2d).
277pub fn conv_transpose2d(
278    x: Tensor<4>,
279    weight: Tensor<4>,
280    bias: Option<Tensor<1>>,
281    options: ConvTransposeOptions<2>,
282) -> Tensor<4> {
283    check!(TensorCheck::conv_transpose(
284        "conv_transpose2d",
285        x.dims(),
286        weight.dims(),
287    ));
288    Tensor::new(BridgeTensor::float(Dispatch::conv_transpose2d(
289        x.primitive.into_float(),
290        weight.primitive.into_float(),
291        bias.map(|b| b.primitive.into_float()),
292        options,
293    )))
294}
295
296/// Applies a 3D transposed convolution](burn_backend::ops::ModuleOps::conv_transpose3d).
297pub fn conv_transpose3d(
298    x: Tensor<5>,
299    weight: Tensor<5>,
300    bias: Option<Tensor<1>>,
301    options: ConvTransposeOptions<3>,
302) -> Tensor<5> {
303    check!(TensorCheck::conv_transpose(
304        "conv_transpose3d",
305        x.dims(),
306        weight.dims(),
307    ));
308    Tensor::new(BridgeTensor::float(Dispatch::conv_transpose3d(
309        x.primitive.into_float(),
310        weight.primitive.into_float(),
311        bias.map(|b| b.primitive.into_float()),
312        options,
313    )))
314}
315
316/// Applies a [4D to 3D unfold](burn_backend::ops::ModuleOps::unfold4d).
317pub fn unfold4d(x: Tensor<4>, kernel_size: [usize; 2], options: UnfoldOptions) -> Tensor<3> {
318    Tensor::new(BridgeTensor::float(Dispatch::unfold4d(
319        x.primitive.into_float(),
320        kernel_size,
321        options,
322    )))
323}
324
325/// Applies a 3D to 4D fold, the inverse of [unfold4d].
326///
327/// Combines an array of sliding local blocks into a large containing tensor, summing the
328/// values of blocks that overlap. This is the operation performed by
329/// [`torch.nn.Fold`](https://pytorch.org/docs/stable/generated/torch.nn.Fold.html), and is the
330/// adjoint of [unfold4d]: it reuses the same one-hot kernel through a [conv_transpose2d].
331///
332/// # Arguments
333///
334/// * `x` - Input columns of shape
335///   `[batch_size, channels * kernel_size_0 * kernel_size_1, number_of_blocks]`.
336/// * `output_size` - The spatial size `[height, width]` of the folded output tensor.
337/// * `kernel_size` - The size of the sliding blocks.
338/// * `options` - The stride, padding and dilation of the matching unfold.
339///
340/// # Returns
341///
342/// A tensor of shape `[batch_size, channels, output_size_0, output_size_1]`.
343pub fn fold4d(
344    x: Tensor<3>,
345    output_size: [usize; 2],
346    kernel_size: [usize; 2],
347    options: UnfoldOptions,
348) -> Tensor<4> {
349    Tensor::new(BridgeTensor::float(Dispatch::fold4d(
350        x.primitive.into_float(),
351        output_size,
352        kernel_size,
353        options,
354    )))
355}
356
357/// Applies a [1D max pooling](burn_backend::ops::ModuleOps::max_pool1d).
358///
359/// Supports symmetric and asymmetric padding through [`MaxPoolOptions`].
360pub fn max_pool1d(x: Tensor<3>, options: MaxPoolOptions<1>) -> Tensor<3> {
361    let dims = x.dims();
362    let (x, padding) = pad_max_pool_input(x, &options);
363    let output = Tensor::new(BridgeTensor::float(Dispatch::max_pool1d(
364        x.primitive.into_float(),
365        options.kernel_size[0],
366        options.stride[0],
367        padding[0],
368        options.dilation[0],
369        options.ceil_mode,
370    )));
371
372    drop_end_padding_windows(
373        output,
374        dims,
375        options.stride,
376        options.padding,
377        options.ceil_mode,
378    )
379}
380
381/// Applies a [2D max pooling](burn_backend::ops::ModuleOps::max_pool2d).
382///
383/// Supports symmetric and asymmetric padding through [`MaxPoolOptions`].
384pub fn max_pool2d(x: Tensor<4>, options: MaxPoolOptions<2>) -> Tensor<4> {
385    let dims = x.dims();
386    let (x, padding) = pad_max_pool_input(x, &options);
387    let output = Tensor::new(BridgeTensor::float(Dispatch::max_pool2d(
388        x.primitive.into_float(),
389        options.kernel_size,
390        options.stride,
391        padding,
392        options.dilation,
393        options.ceil_mode,
394    )));
395
396    drop_end_padding_windows(
397        output,
398        dims,
399        options.stride,
400        options.padding,
401        options.ceil_mode,
402    )
403}
404
405/// Applies a [2D avg pooling](burn_backend::ops::ModuleOps::avg_pool2d).
406///
407/// Supports symmetric and asymmetric padding through [`AvgPoolOptions`].
408pub fn avg_pool2d(x: Tensor<4>, options: AvgPoolOptions<2>) -> Tensor<4> {
409    avg_pool(x, &options, |x, padding, count_include_pad| {
410        Tensor::new(BridgeTensor::float(Dispatch::avg_pool2d(
411            x.primitive.into_float(),
412            options.kernel_size,
413            options.stride,
414            padding,
415            count_include_pad,
416            options.ceil_mode,
417        )))
418    })
419}
420
421/// Applies a [1D avg pooling](burn_backend::ops::ModuleOps::avg_pool1d).
422///
423/// Supports symmetric and asymmetric padding through [`AvgPoolOptions`].
424pub fn avg_pool1d(x: Tensor<3>, options: AvgPoolOptions<1>) -> Tensor<3> {
425    avg_pool(x, &options, |x, padding, count_include_pad| {
426        Tensor::new(BridgeTensor::float(Dispatch::avg_pool1d(
427            x.primitive.into_float(),
428            options.kernel_size[0],
429            options.stride[0],
430            padding[0],
431            count_include_pad,
432            options.ceil_mode,
433        )))
434    })
435}
436
437/// Applies a [1D max pooling with indices](burn_backend::ops::ModuleOps::max_pool1d_with_indices).
438///
439/// Supports symmetric and asymmetric padding through [`MaxPoolOptions`].
440/// Returned indices always refer to positions in the unpadded input.
441pub fn max_pool1d_with_indices(
442    x: Tensor<3>,
443    options: MaxPoolOptions<1>,
444) -> (Tensor<3>, Tensor<3, Int>) {
445    let dims = x.dims();
446    let [_, _, length] = dims;
447    let indices_dtype = x.device().get_or_init_settings().int_dtype;
448    let (x, padding) = pad_max_pool_input(x, &options);
449    let output = Dispatch::max_pool1d_with_indices(
450        x.primitive.into_float(),
451        options.kernel_size[0],
452        options.stride[0],
453        padding[0],
454        options.dilation[0],
455        options.ceil_mode,
456        indices_dtype,
457    );
458    let mut indices = Tensor::<3, Int>::new(BridgeTensor::int(output.indices));
459
460    if options.is_asymmetric() {
461        let (left, _) = options.padding[0];
462        indices = unpad_indices(indices, left, length);
463    }
464
465    let output = Tensor::new(BridgeTensor::float(output.output));
466    let MaxPoolOptions {
467        stride,
468        padding,
469        ceil_mode,
470        ..
471    } = options;
472    (
473        drop_end_padding_windows(output, dims, stride, padding, ceil_mode),
474        drop_end_padding_windows(indices, dims, stride, padding, ceil_mode),
475    )
476}
477
478/// Applies a [2D max pooling with indices](burn_backend::ops::ModuleOps::max_pool2d_with_indices).
479///
480/// Supports symmetric and asymmetric padding through [`MaxPoolOptions`].
481/// Returned indices always refer to positions in the unpadded input.
482pub fn max_pool2d_with_indices(
483    x: Tensor<4>,
484    options: MaxPoolOptions<2>,
485) -> (Tensor<4>, Tensor<4, Int>) {
486    let dims = x.dims();
487    let [_, _, height, width] = dims;
488    let indices_dtype = x.device().get_or_init_settings().int_dtype;
489    let (x, padding) = pad_max_pool_input(x, &options);
490    let output = Dispatch::max_pool2d_with_indices(
491        x.primitive.into_float(),
492        options.kernel_size,
493        options.stride,
494        padding,
495        options.dilation,
496        options.ceil_mode,
497        indices_dtype,
498    );
499    let mut indices = Tensor::<4, Int>::new(BridgeTensor::int(output.indices));
500
501    if options.is_asymmetric() {
502        let [(top, _), (left, right)] = options.padding;
503        let width_padded = width + left + right;
504        let rows = unpad_indices(indices.clone().div_scalar(width_padded as i64), top, height);
505        let cols = unpad_indices(indices.remainder_scalar(width_padded as i64), left, width);
506        indices = rows.mul_scalar(width as i64).add(cols);
507    }
508
509    let output = Tensor::new(BridgeTensor::float(output.output));
510    let MaxPoolOptions {
511        stride,
512        padding,
513        ceil_mode,
514        ..
515    } = options;
516    (
517        drop_end_padding_windows(output, dims, stride, padding, ceil_mode),
518        drop_end_padding_windows(indices, dims, stride, padding, ceil_mode),
519    )
520}
521
522/// When any dimension has asymmetric padding, materializes all padding with `-inf`
523/// so the backend pools without padding.
524///
525/// Returns the input unchanged along with the backend padding when it is symmetric.
526fn pad_max_pool_input<const D: usize, const N: usize>(
527    x: Tensor<D>,
528    options: &MaxPoolOptions<N>,
529) -> (Tensor<D>, [usize; N]) {
530    if options.is_asymmetric() {
531        (
532            x.pad(options.padding, PadMode::Constant(f32::NEG_INFINITY)),
533            [0; N],
534        )
535    } else {
536        (x, options.padding.map(|(begin, _)| begin))
537    }
538}
539
540/// Maps positions along one padded dimension back to the unpadded input.
541///
542/// A window made only of padding and `-inf` inputs may select a padded position,
543/// which is clamped to the nearest input position.
544fn unpad_indices<const D: usize>(
545    indices: Tensor<D, Int>,
546    begin: usize,
547    size: usize,
548) -> Tensor<D, Int> {
549    indices.sub_scalar(begin as i64).clamp(0, size as i64 - 1)
550}
551
552/// Average pooling with asymmetric padding support.
553///
554/// `pool` runs the backend operation with the given symmetric padding and
555/// `count_include_pad` flag.
556fn avg_pool<const D: usize, const N: usize>(
557    x: Tensor<D>,
558    options: &AvgPoolOptions<N>,
559    pool: impl Fn(Tensor<D>, [usize; N], bool) -> Tensor<D>,
560) -> Tensor<D> {
561    if !options.is_asymmetric() {
562        let padding = options.padding.map(|(begin, _)| begin);
563        return pool(x, padding, options.count_include_pad);
564    }
565
566    let dims = x.dims();
567    let valid = (!options.count_include_pad).then(|| {
568        // Only the spatial dimensions matter for the validity mask.
569        let mut shape = [1; D];
570        shape[D - N..].copy_from_slice(&dims[D - N..]);
571        Tensor::<D>::ones(shape, (&x.device(), x.dtype()))
572            .pad(options.padding, PadMode::Constant(0.0))
573    });
574    let output = pool(
575        x.pad(options.padding, PadMode::Constant(0.0)),
576        [0; N],
577        options.count_include_pad,
578    );
579
580    let output = match valid {
581        // Materialized padding is indistinguishable from input to the backend. Pooling a
582        // validity mask with the same settings recovers the fraction of real values in
583        // each window, including partial windows created by ceil mode.
584        Some(valid) => {
585            let valid = pool(valid, [0; N], false);
586            let empty = valid.clone().equal_elem(0.0);
587            output / valid.mask_fill(empty, 1.0)
588        }
589        None => output,
590    };
591
592    drop_end_padding_windows(
593        output,
594        dims,
595        options.stride,
596        options.padding,
597        options.ceil_mode,
598    )
599}
600
601/// In ceil mode, drops the last window of each spatial dimension when it starts in the
602/// end padding, matching PyTorch and ONNX.
603///
604/// Backends only drop such windows for padding they apply themselves, so one extra window
605/// survives once asymmetric padding has been materialized. For backend-applied padding
606/// the condition never holds and the output is returned unchanged.
607///
608/// One window is all there is to drop when the end padding is smaller than the kernel (the
609/// range PyTorch and ONNX Runtime accept): floor mode never starts a window in it, and ceil
610/// mode adds at most one. Larger end padding keeps floor-mode windows there, and only the
611/// extra ceil window is dropped, as in the PyTorch and ONNX output-size formulas.
612fn drop_end_padding_windows<const D: usize, const N: usize, K: Basic>(
613    output: Tensor<D, K>,
614    input_dims: [usize; D],
615    stride: [usize; N],
616    padding: [(usize, usize); N],
617    ceil_mode: bool,
618) -> Tensor<D, K> {
619    if !ceil_mode {
620        return output;
621    }
622
623    (0..N).fold(output, |output, i| {
624        let dim = D - N + i;
625        let size = output.dims()[dim];
626        let (begin, _) = padding[i];
627        if size > 1 && (size - 1) * stride[i] >= input_dims[dim] + begin {
628            output.narrow(dim, 0, size - 1)
629        } else {
630            output
631        }
632    })
633}
634
635/// Applies a [2D adaptive avg pooling](burn_backend::ops::ModuleOps::adaptive_avg_pool2d).
636pub fn adaptive_avg_pool2d(x: Tensor<4>, output_size: [usize; 2]) -> Tensor<4> {
637    Tensor::new(BridgeTensor::float(Dispatch::adaptive_avg_pool2d(
638        x.primitive.into_float(),
639        output_size,
640    )))
641}
642
643/// Applies a [3D adaptive avg pooling](burn_backend::ops::ModuleOps::adaptive_avg_pool3d).
644pub fn adaptive_avg_pool3d(x: Tensor<5>, output_size: [usize; 3]) -> Tensor<5> {
645    Tensor::new(BridgeTensor::float(Dispatch::adaptive_avg_pool3d(
646        x.primitive.into_float(),
647        output_size,
648    )))
649}
650
651/// Applies a [1D adaptive avg pooling](burn_backend::ops::ModuleOps::adaptive_avg_pool1d).
652pub fn adaptive_avg_pool1d(x: Tensor<3>, output_size: usize) -> Tensor<3> {
653    Tensor::new(BridgeTensor::float(Dispatch::adaptive_avg_pool1d(
654        x.primitive.into_float(),
655        output_size,
656    )))
657}
658
659/// Applies a [2D interpolation](burn_backend::ops::ModuleOps::interpolate).
660///
661/// The output spatial size is taken from `options.output_size`, or computed as
662/// `floor(input_size * scale_factor)` from `options.scale_factor`.
663///
664/// # Panics
665///
666/// Panics unless exactly one of `output_size` or `scale_factor` is set, or if the
667/// scaled size exceeds `usize::MAX`.
668///
669/// # Example
670///
671/// ```rust,ignore
672/// // Resize to a fixed size.
673/// interpolate(x, InterpolateOptions::new(mode).with_output_size([224, 224]));
674/// // Upsample by 2x.
675/// interpolate(x, InterpolateOptions::new(mode).with_scale_factor([2.0, 2.0]));
676/// ```
677pub fn interpolate(x: Tensor<4>, options: InterpolateOptions) -> Tensor<4> {
678    let [_, _, h, w] = x.dims();
679    let output_size = interpolate_output_size([h, w], &options);
680    Tensor::new(BridgeTensor::float(Dispatch::interpolate(
681        x.primitive.into_float(),
682        output_size,
683        options,
684    )))
685}
686
687fn interpolate_output_size(input_size: [usize; 2], options: &InterpolateOptions) -> [usize; 2] {
688    match (options.output_size, options.scale_factor) {
689        (Some(output_size), None) => output_size,
690        (None, Some(scale_factor)) => core::array::from_fn(|i| {
691            let size = input_size[i] as f64 * scale_factor[i] as f64;
692            assert!(
693                size <= usize::MAX as f64,
694                "Interpolate scale factor {} is too large for input size {}",
695                scale_factor[i],
696                input_size[i]
697            );
698            size as usize
699        }),
700        (Some(_), Some(_)) => {
701            panic!("Interpolate options must set only one of output_size or scale_factor")
702        }
703        (None, None) => panic!("Interpolate options must set output_size or scale_factor"),
704    }
705}
706
707/// Applies a linear transformation to the input tensor using the given weight and bias.
708///
709/// ```math
710/// y = x @ weight + [bias]
711/// ```
712///
713/// # Arguments:
714///
715/// - `input` is the input tensor, ``[..., d_input]``.
716/// - `weight` is the weight tensor, ``[d_input, d_output]``.
717/// - `bias` is the bias tensor (optional), ``[d_output]``.
718///
719/// # Returns:
720///
721/// The transformed tensor, ``[..., d_output]``.
722///
723/// # Compatibility
724///
725/// This function differs from PyTorch's ``torch.nn.functional.linear`` in that it does not
726/// transpose the weight matrix. In PyTorch, the weight matrix is transposed before
727/// multiplication:
728///
729/// ```math
730/// y = x @ weight^T + [bias]
731/// ```
732pub fn linear<const D: usize>(
733    input: Tensor<D>,
734    weight: Tensor<2>,
735    bias: Option<Tensor<1>>,
736) -> Tensor<D> {
737    if D == 1 {
738        // Insert and remove an extra batch dimension for the batch matmul to work.
739        let input = input.unsqueeze::<2>();
740        let output = linear(input, weight, bias);
741        return output.squeeze_dim(0);
742    }
743
744    // A quantized weight must stay quantized: `linear_impl` converts its
745    // operands to float, which would dequantize (materialize) the whole weight
746    // matrix on every forward. Route through the quantized matmul instead, which
747    // streams the packed weight directly — but reuse the same batch-fold policy
748    // the float `linear` applies, so a decode-shaped call folds its batches into
749    // the rows for one `[rows, d_in] @ [d_in, d_out]` matmul rather than a
750    // broadcast batched matmul that re-reads the packed weight per batch.
751    if let DType::QFloat(_) = weight.dtype() {
752        let dims = input.dims();
753        let analysis = MatmulTransformAnalysis::from_shapes(&input.shape(), &weight.shape());
754
755        let output = match MatmulTransformPolicy::default().action(&analysis) {
756            MatmulTransformAction::MergeBatches { rows } => {
757                let d_in = dims[D - 1];
758                let d_out = weight.dims()[1];
759
760                let folded = input.reshape([rows, d_in]).matmul(weight);
761
762                let mut out_dims = dims;
763                out_dims[D - 1] = d_out;
764                folded.reshape(out_dims)
765            }
766            MatmulTransformAction::Keep => input.matmul(weight.unsqueeze::<D>()),
767        };
768
769        return match bias {
770            Some(bias) => output + bias.unsqueeze(),
771            None => output,
772        };
773    }
774
775    Tensor::new(linear_impl(
776        input.primitive,
777        weight.primitive,
778        bias.map(|b| b.primitive),
779    ))
780}
781
782fn linear_impl(
783    input: BridgeTensor,
784    weight: BridgeTensor,
785    bias: Option<BridgeTensor>,
786) -> BridgeTensor {
787    BridgeTensor::float(Dispatch::linear(
788        input.into_float(),
789        weight.into_float(),
790        bias.map(|b| b.into_float()),
791    ))
792}
793
794/// Computes scaled dot-product attention: softmax(QKᵗ * scale) · V,
795/// where scale defaults to 1/sqrt(head_dim) (configurable via `options.scale`).
796/// Optionally applies masking, additive bias, causal masking, and softcap.
797///
798/// # Arguments
799/// - `query`: Query tensor of shape `[batch_size, num_heads, seq_len_q, head_dim]`
800/// - `key`: Key tensor of shape `[batch_size, num_heads, seq_len_k, head_dim]`
801/// - `value`: Value tensor of shape `[batch_size, num_heads, seq_len_k, val_dim]`
802/// - `mask`: Optional boolean mask of shape `[batch_size, num_heads, seq_len_q, seq_len_k]`,
803///   where `true` indicates positions to mask (i.e. set to -inf before softmax).
804/// - `attn_bias`: Optional float tensor of shape `[batch_size, num_heads, seq_len_q, seq_len_k]`
805///   added to the attention scores before softmax (e.g. ALiBi, relative position biases).
806/// - `options`: Additional attention options (custom scale, softcap, causal masking).
807///
808/// # Returns
809/// A tensor of shape `[batch_size, num_heads, seq_len_q, val_dim]`
810/// representing the attended context per head.
811///
812/// # Note
813/// This implementation does not support dropout and is intended for inference or
814/// use cases where dropout is not needed.
815pub fn attention(
816    query: Tensor<4>,
817    key: Tensor<4>,
818    value: Tensor<4>,
819    mask: Option<Tensor<4, Bool>>,
820    attn_bias: Option<Tensor<4>>,
821    options: AttentionModuleOptions,
822) -> Tensor<4> {
823    Tensor::new(BridgeTensor::float(Dispatch::attention(
824        query.primitive.into_float(),
825        key.primitive.into_float(),
826        value.primitive.into_float(),
827        mask.map(|mask| mask.primitive.into()),
828        attn_bias.map(|bias| bias.primitive.into_float()),
829        options,
830    )))
831}
832
833/// Exports attention fallback to test backend's attention against.
834pub fn attention_fallback(
835    query: Tensor<4>,
836    key: Tensor<4>,
837    value: Tensor<4>,
838    mask: Option<Tensor<4, Bool>>,
839    attn_bias: Option<Tensor<4>>,
840    options: AttentionModuleOptions,
841) -> Tensor<4> {
842    Tensor::new(BridgeTensor::float(
843        burn_backend::ops::attention::attention_fallback::<Dispatch>(
844            query.primitive.into_float(),
845            key.primitive.into_float(),
846            value.primitive.into_float(),
847            mask.map(|mask| mask.primitive.into()),
848            attn_bias.map(|bias| bias.primitive.into_float()),
849            options,
850        ),
851    ))
852}
853
854/// Calculate the [2D convolution](burn_backend::ops::ModuleOps::conv2d) backward pass, returning the gradient for `weight`.
855pub fn conv2d_weight_backward(
856    x: Tensor<4>,
857    weight: Tensor<4>,
858    output_grad: Tensor<4>,
859    options: ConvOptions<2>,
860) -> Tensor<4> {
861    Tensor::new(BridgeTensor::float(Dispatch::conv2d_weight_backward(
862        x.primitive.into_float(),
863        weight.primitive.into_float(),
864        output_grad.primitive.into_float(),
865        options,
866    )))
867}
868
869/// Backward pass for the [avg pooling 2d](ModuleOps::avg_pool2d) operation.
870pub fn avg_pool2d_backward(
871    x: Tensor<4>,
872    grad: Tensor<4>,
873    kernel_size: [usize; 2],
874    stride: [usize; 2],
875    padding: [usize; 2],
876    count_include_pad: bool,
877    ceil_mode: bool,
878) -> Tensor<4> {
879    Tensor::new(BridgeTensor::float(Dispatch::avg_pool2d_backward(
880        x.primitive.into_float(),
881        grad.primitive.into_float(),
882        kernel_size,
883        stride,
884        padding,
885        count_include_pad,
886        ceil_mode,
887    )))
888}
889
890/// Backward pass for the [max pooling 2d](ModuleOps::max_pool2d_with_indices) operation.
891#[allow(clippy::too_many_arguments)]
892pub fn max_pool2d_with_indices_backward(
893    x: Tensor<4>,
894    kernel_size: [usize; 2],
895    stride: [usize; 2],
896    padding: [usize; 2],
897    dilation: [usize; 2],
898    ceil_mode: bool,
899    output_grad: Tensor<4>,
900    indices: Tensor<4, Int>,
901) -> Tensor<4> {
902    Tensor::new(BridgeTensor::float(
903        Dispatch::max_pool2d_with_indices_backward(
904            x.primitive.into_float(),
905            kernel_size,
906            stride,
907            padding,
908            dilation,
909            ceil_mode,
910            output_grad.primitive.into_float(),
911            indices.primitive.into(),
912        )
913        .x_grad,
914    ))
915}
916
917/// Applies Layer Normalization over the last dimension of the input tensor.
918///
919/// Computes `(x - mean) / sqrt(var + epsilon) * gamma + beta`, where `mean` and
920/// (biased) `var` are reduced over the last axis.
921///
922/// # Shapes
923///
924/// - input: `[..., any, d_model]`
925/// - output: `[..., any, d_model]`
926pub fn layer_norm<const D: usize>(
927    input: Tensor<D>,
928    gamma: Tensor<1>,
929    beta: Option<Tensor<1>>,
930    epsilon: f64,
931) -> Tensor<D> {
932    Tensor::new(layer_norm_impl(
933        input.primitive,
934        gamma.primitive,
935        beta.map(|b| b.primitive),
936        epsilon,
937    ))
938}
939
940fn layer_norm_impl(
941    input: BridgeTensor,
942    gamma: BridgeTensor,
943    beta: Option<BridgeTensor>,
944    epsilon: f64,
945) -> BridgeTensor {
946    BridgeTensor::float(Dispatch::layer_norm(
947        input.into_float(),
948        gamma.into_float(),
949        beta.map(|b| b.into_float()),
950        epsilon,
951    ))
952}
953
954#[cfg(test)]
955mod tests {
956    use super::*;
957    use crate::ops::InterpolateMode;
958
959    fn options() -> InterpolateOptions {
960        InterpolateOptions::new(InterpolateMode::Nearest)
961    }
962
963    #[test]
964    fn interpolate_output_size_from_output_size() {
965        let size = interpolate_output_size([4, 4], &options().with_output_size([2, 3]));
966        assert_eq!(size, [2, 3]);
967    }
968
969    #[test]
970    fn interpolate_output_size_from_scale_factor_floors() {
971        let size = interpolate_output_size([4, 5], &options().with_scale_factor([2.0, 1.5]));
972        assert_eq!(size, [8, 7]);
973    }
974
975    #[test]
976    fn interpolate_options_last_sizing_builder_wins() {
977        let size = interpolate_output_size(
978            [4, 4],
979            &options()
980                .with_output_size([2, 2])
981                .with_scale_factor([2.0, 2.0]),
982        );
983        assert_eq!(size, [8, 8]);
984    }
985
986    #[test]
987    #[should_panic(expected = "must set output_size or scale_factor")]
988    fn interpolate_output_size_requires_size() {
989        interpolate_output_size([4, 4], &options());
990    }
991
992    #[test]
993    #[should_panic(expected = "only one of output_size or scale_factor")]
994    fn interpolate_output_size_rejects_both() {
995        let mut options = options().with_output_size([2, 2]);
996        options.scale_factor = Some([2.0, 2.0]);
997        interpolate_output_size([4, 4], &options);
998    }
999
1000    #[test]
1001    #[should_panic(expected = "too large")]
1002    fn interpolate_output_size_rejects_overflow() {
1003        interpolate_output_size(
1004            [4, usize::MAX - 1],
1005            &options().with_scale_factor([1.0, 2.0]),
1006        );
1007    }
1008}