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    ops::{
9        AttentionModuleOptions, BridgeTensor, ConvOptions, ConvTransposeOptions, DeformConvOptions,
10        InterpolateOptions, PadMode, PaddedConvOptions, UnfoldOptions,
11    },
12};
13
14/// Computes the [CTC loss](burn_backend::ops::ModuleOps::ctc_loss).
15///
16/// # Arguments
17///
18/// * `log_probs` - Log-probabilities of shape `[T, N, C]`
19/// * `targets` - Target label indices of shape `[N, S]`
20/// * `input_lengths` - Actual input sequence lengths per batch element `[N]`
21/// * `target_lengths` - Actual target lengths per batch element `[N]`
22/// * `blank` - Index of the blank label
23///
24/// # Returns
25///
26/// Per-sample loss of shape `[N]`
27pub fn ctc_loss(
28    log_probs: Tensor<3>,
29    targets: Tensor<2, Int>,
30    input_lengths: Tensor<1, Int>,
31    target_lengths: Tensor<1, Int>,
32    blank: usize,
33) -> Tensor<1> {
34    Tensor::new(BridgeTensor::float(Dispatch::ctc_loss(
35        log_probs.primitive.into_float(),
36        targets.primitive.into(),
37        input_lengths.primitive.into(),
38        target_lengths.primitive.into(),
39        blank,
40    )))
41}
42
43/// Applies the [embedding module](burn_backend::ops::ModuleOps::embedding).
44pub fn embedding(weights: Tensor<2>, indices: Tensor<2, Int>) -> Tensor<3> {
45    Tensor::new(BridgeTensor::float(Dispatch::embedding(
46        weights.primitive.into_float(),
47        indices.primitive.into(),
48    )))
49}
50
51/// Applies a [1D convolution](burn_backend::ops::ModuleOps::conv1d).
52///
53/// Accepts [`ConvOptions`] for symmetric padding, or [`PaddedConvOptions`] for
54/// asymmetric padding. When asymmetric padding is specified, an explicit pad
55/// operation is applied before the convolution backend op.
56pub fn conv1d(
57    x: Tensor<3>,
58    weight: Tensor<3>,
59    bias: Option<Tensor<1>>,
60    options: impl Into<PaddedConvOptions<1>>,
61) -> Tensor<3> {
62    let padded_options = options.into();
63    check!(TensorCheck::conv(
64        "conv1d",
65        x.dims(),
66        weight.dims(),
67        padded_options.options.groups,
68    ));
69
70    if let Some(padding_end) = padded_options.padding_end {
71        let left = padded_options.options.padding[0];
72        let right = padding_end[0];
73        // For 1D (NCL format), pad the length dimension
74        let padded = x.pad((left, right, 0, 0), PadMode::Constant(0.0));
75        let zero_options = ConvOptions::new(
76            padded_options.options.stride,
77            [0],
78            padded_options.options.dilation,
79            padded_options.options.groups,
80        );
81        Tensor::new(BridgeTensor::float(Dispatch::conv1d(
82            padded.primitive.into_float(),
83            weight.primitive.into_float(),
84            bias.map(|b| b.primitive.into_float()),
85            zero_options,
86        )))
87    } else {
88        Tensor::new(BridgeTensor::float(Dispatch::conv1d(
89            x.primitive.into_float(),
90            weight.primitive.into_float(),
91            bias.map(|b| b.primitive.into_float()),
92            padded_options.options,
93        )))
94    }
95}
96
97/// Applies a [2D convolution](burn_backend::ops::ModuleOps::conv2d).
98///
99/// Accepts [`ConvOptions`] for symmetric padding, or [`PaddedConvOptions`] for
100/// asymmetric padding. When asymmetric padding is specified, an explicit pad
101/// operation is applied before the convolution backend op.
102pub fn conv2d(
103    x: Tensor<4>,
104    weight: Tensor<4>,
105    bias: Option<Tensor<1>>,
106    options: impl Into<PaddedConvOptions<2>>,
107) -> Tensor<4> {
108    let padded_options = options.into();
109    check!(TensorCheck::conv(
110        "conv2d",
111        x.dims(),
112        weight.dims(),
113        padded_options.options.groups,
114    ));
115
116    if let Some(padding_end) = padded_options.padding_end {
117        let top = padded_options.options.padding[0];
118        let left = padded_options.options.padding[1];
119        let bottom = padding_end[0];
120        let right = padding_end[1];
121        // For 2D (NCHW format), pad height and width
122        let padded = x.pad((left, right, top, bottom), PadMode::Constant(0.0));
123        let zero_options = ConvOptions::new(
124            padded_options.options.stride,
125            [0, 0],
126            padded_options.options.dilation,
127            padded_options.options.groups,
128        );
129        Tensor::new(BridgeTensor::float(Dispatch::conv2d(
130            padded.primitive.into_float(),
131            weight.primitive.into_float(),
132            bias.map(|b| b.primitive.into_float()),
133            zero_options,
134        )))
135    } else {
136        Tensor::new(BridgeTensor::float(Dispatch::conv2d(
137            x.primitive.into_float(),
138            weight.primitive.into_float(),
139            bias.map(|b| b.primitive.into_float()),
140            padded_options.options,
141        )))
142    }
143}
144
145/// Applies a [3D convolution](burn_backend::ops::ModuleOps::conv3d).
146///
147/// Accepts [`ConvOptions`] for symmetric padding, or [`PaddedConvOptions`] for
148/// asymmetric padding. Asymmetric 3D padding is not yet supported.
149pub fn conv3d(
150    x: Tensor<5>,
151    weight: Tensor<5>,
152    bias: Option<Tensor<1>>,
153    options: impl Into<PaddedConvOptions<3>>,
154) -> Tensor<5> {
155    let padded_options = options.into();
156    check!(TensorCheck::conv(
157        "conv3d",
158        x.dims(),
159        weight.dims(),
160        padded_options.options.groups,
161    ));
162
163    if padded_options.is_asymmetric() {
164        panic!("Asymmetric padding is not yet supported for conv3d");
165    }
166
167    Tensor::new(BridgeTensor::float(Dispatch::conv3d(
168        x.primitive.into_float(),
169        weight.primitive.into_float(),
170        bias.map(|b| b.primitive.into_float()),
171        padded_options.options,
172    )))
173}
174
175/// Applies a [Deformable 2D convolution](burn_backend::ops::ModuleOps::deform_conv2d).
176pub fn deform_conv2d(
177    x: Tensor<4>,
178    offset: Tensor<4>,
179    weight: Tensor<4>,
180    mask: Option<Tensor<4>>,
181    bias: Option<Tensor<1>>,
182    options: DeformConvOptions<2>,
183) -> Tensor<4> {
184    check!(TensorCheck::conv(
185        "deform_conv2d",
186        x.dims(),
187        weight.dims(),
188        options.weight_groups,
189    ));
190    Tensor::new(BridgeTensor::float(Dispatch::deform_conv2d(
191        x.primitive.into_float(),
192        offset.primitive.into_float(),
193        weight.primitive.into_float(),
194        mask.map(|m| m.primitive.into_float()),
195        bias.map(|b| b.primitive.into_float()),
196        options,
197    )))
198}
199
200/// Applies a [1D transposed convolution](burn_backend::ops::ModuleOps::conv_transpose1d).
201pub fn conv_transpose1d(
202    x: Tensor<3>,
203    weight: Tensor<3>,
204    bias: Option<Tensor<1>>,
205    options: ConvTransposeOptions<1>,
206) -> Tensor<3> {
207    check!(TensorCheck::conv_transpose(
208        "conv_transpose1d",
209        x.dims(),
210        weight.dims(),
211    ));
212    Tensor::new(BridgeTensor::float(Dispatch::conv_transpose1d(
213        x.primitive.into_float(),
214        weight.primitive.into_float(),
215        bias.map(|b| b.primitive.into_float()),
216        options,
217    )))
218}
219
220/// Applies a [2D transposed convolution](burn_backend::ops::ModuleOps::conv_transpose2d).
221pub fn conv_transpose2d(
222    x: Tensor<4>,
223    weight: Tensor<4>,
224    bias: Option<Tensor<1>>,
225    options: ConvTransposeOptions<2>,
226) -> Tensor<4> {
227    check!(TensorCheck::conv_transpose(
228        "conv_transpose2d",
229        x.dims(),
230        weight.dims(),
231    ));
232    Tensor::new(BridgeTensor::float(Dispatch::conv_transpose2d(
233        x.primitive.into_float(),
234        weight.primitive.into_float(),
235        bias.map(|b| b.primitive.into_float()),
236        options,
237    )))
238}
239
240/// Applies a 3D transposed convolution](burn_backend::ops::ModuleOps::conv_transpose3d).
241pub fn conv_transpose3d(
242    x: Tensor<5>,
243    weight: Tensor<5>,
244    bias: Option<Tensor<1>>,
245    options: ConvTransposeOptions<3>,
246) -> Tensor<5> {
247    check!(TensorCheck::conv_transpose(
248        "conv_transpose3d",
249        x.dims(),
250        weight.dims(),
251    ));
252    Tensor::new(BridgeTensor::float(Dispatch::conv_transpose3d(
253        x.primitive.into_float(),
254        weight.primitive.into_float(),
255        bias.map(|b| b.primitive.into_float()),
256        options,
257    )))
258}
259
260/// Applies a [4D to 3D unfold](burn_backend::ops::ModuleOps::unfold4d).
261pub fn unfold4d(x: Tensor<4>, kernel_size: [usize; 2], options: UnfoldOptions) -> Tensor<3> {
262    Tensor::new(BridgeTensor::float(Dispatch::unfold4d(
263        x.primitive.into_float(),
264        kernel_size,
265        options,
266    )))
267}
268
269/// Applies a [1D max pooling](burn_backend::ops::ModuleOps::max_pool1d).
270pub fn max_pool1d(
271    x: Tensor<3>,
272    kernel_size: usize,
273    stride: usize,
274    padding: usize,
275    dilation: usize,
276    ceil_mode: bool,
277) -> Tensor<3> {
278    Tensor::new(BridgeTensor::float(Dispatch::max_pool1d(
279        x.primitive.into_float(),
280        kernel_size,
281        stride,
282        padding,
283        dilation,
284        ceil_mode,
285    )))
286}
287
288/// Applies a [2D max pooling](burn_backend::ops::ModuleOps::max_pool2d).
289pub fn max_pool2d(
290    x: Tensor<4>,
291    kernel_size: [usize; 2],
292    stride: [usize; 2],
293    padding: [usize; 2],
294    dilation: [usize; 2],
295    ceil_mode: bool,
296) -> Tensor<4> {
297    Tensor::new(BridgeTensor::float(Dispatch::max_pool2d(
298        x.primitive.into_float(),
299        kernel_size,
300        stride,
301        padding,
302        dilation,
303        ceil_mode,
304    )))
305}
306
307/// Applies a [2D avg pooling](burn_backend::ops::ModuleOps::avg_pool2d).
308pub fn avg_pool2d(
309    x: Tensor<4>,
310    kernel_size: [usize; 2],
311    stride: [usize; 2],
312    padding: [usize; 2],
313    count_include_pad: bool,
314    ceil_mode: bool,
315) -> Tensor<4> {
316    Tensor::new(BridgeTensor::float(Dispatch::avg_pool2d(
317        x.primitive.into_float(),
318        kernel_size,
319        stride,
320        padding,
321        count_include_pad,
322        ceil_mode,
323    )))
324}
325
326/// Applies a [1D avg pooling](burn_backend::ops::ModuleOps::avg_pool1d).
327pub fn avg_pool1d(
328    x: Tensor<3>,
329    kernel_size: usize,
330    stride: usize,
331    padding: usize,
332    count_include_pad: bool,
333    ceil_mode: bool,
334) -> Tensor<3> {
335    Tensor::new(BridgeTensor::float(Dispatch::avg_pool1d(
336        x.primitive.into_float(),
337        kernel_size,
338        stride,
339        padding,
340        count_include_pad,
341        ceil_mode,
342    )))
343}
344
345/// Applies a [1D max pooling](burn_backend::ops::ModuleOps::max_pool1d).
346pub fn max_pool1d_with_indices(
347    x: Tensor<3>,
348    kernel_size: usize,
349    stride: usize,
350    padding: usize,
351    dilation: usize,
352    ceil_mode: bool,
353) -> (Tensor<3>, Tensor<3, Int>) {
354    let indices_dtype = x.device().settings().int_dtype;
355    let output = Dispatch::max_pool1d_with_indices(
356        x.primitive.into_float(),
357        kernel_size,
358        stride,
359        padding,
360        dilation,
361        ceil_mode,
362        indices_dtype,
363    );
364
365    (
366        Tensor::new(BridgeTensor::float(output.output)),
367        Tensor::new(BridgeTensor::int(output.indices)),
368    )
369}
370
371/// Applies a [2D max pooling with indices](burn_backend::ops::ModuleOps::max_pool2d_with_indices).
372pub fn max_pool2d_with_indices(
373    x: Tensor<4>,
374    kernel_size: [usize; 2],
375    stride: [usize; 2],
376    padding: [usize; 2],
377    dilation: [usize; 2],
378    ceil_mode: bool,
379) -> (Tensor<4>, Tensor<4, Int>) {
380    let indices_dtype = x.device().settings().int_dtype;
381    let output = Dispatch::max_pool2d_with_indices(
382        x.primitive.into_float(),
383        kernel_size,
384        stride,
385        padding,
386        dilation,
387        ceil_mode,
388        indices_dtype,
389    );
390
391    (
392        Tensor::new(BridgeTensor::float(output.output)),
393        Tensor::new(BridgeTensor::int(output.indices)),
394    )
395}
396
397/// Applies a [2D adaptive avg pooling](burn_backend::ops::ModuleOps::adaptive_avg_pool2d).
398pub fn adaptive_avg_pool2d(x: Tensor<4>, output_size: [usize; 2]) -> Tensor<4> {
399    Tensor::new(BridgeTensor::float(Dispatch::adaptive_avg_pool2d(
400        x.primitive.into_float(),
401        output_size,
402    )))
403}
404
405/// Applies a [1D adaptive avg pooling](burn_backend::ops::ModuleOps::adaptive_avg_pool1d).
406pub fn adaptive_avg_pool1d(x: Tensor<3>, output_size: usize) -> Tensor<3> {
407    Tensor::new(BridgeTensor::float(Dispatch::adaptive_avg_pool1d(
408        x.primitive.into_float(),
409        output_size,
410    )))
411}
412
413/// Applies a [2D interpolation](burn_backend::ops::ModuleOps::interpolate).
414pub fn interpolate(
415    x: Tensor<4>,
416    output_size: [usize; 2],
417    options: InterpolateOptions,
418) -> Tensor<4> {
419    Tensor::new(BridgeTensor::float(Dispatch::interpolate(
420        x.primitive.into_float(),
421        output_size,
422        options,
423    )))
424}
425
426/// Applies a linear transformation to the input tensor using the given weight and bias.
427///
428/// ```math
429/// y = x @ weight + [bias]
430/// ```
431///
432/// # Arguments:
433///
434/// - `input` is the input tensor, ``[..., d_input]``.
435/// - `weight` is the weight tensor, ``[d_input, d_output]``.
436/// - `bias` is the bias tensor (optional), ``[d_output]``.
437///
438/// # Returns:
439///
440/// The transformed tensor, ``[..., d_output]``.
441///
442/// # Compatibility
443///
444/// This function differs from PyTorch's ``torch.nn.functional.linear`` in that it does not
445/// transpose the weight matrix. In PyTorch, the weight matrix is transposed before
446/// multiplication:
447///
448/// ```math
449/// y = x @ weight^T + [bias]
450/// ```
451pub fn linear<const D: usize>(
452    input: Tensor<D>,
453    weight: Tensor<2>,
454    bias: Option<Tensor<1>>,
455) -> Tensor<D> {
456    if D == 1 {
457        // Insert and remove an extra batch dimension for the batch matmul to work.
458        let input = input.unsqueeze::<2>();
459        let output = linear(input, weight, bias);
460        return output.squeeze_dim(0);
461    }
462
463    // A quantized weight must stay quantized: `linear_impl` converts its
464    // operands to float, which would dequantize (materialize) the whole weight
465    // matrix on every forward. Route through the quantized matmul instead, which
466    // streams the packed weight directly — but reuse the same batch-fold policy
467    // the float `linear` applies, so a decode-shaped call folds its batches into
468    // the rows for one `[rows, d_in] @ [d_in, d_out]` matmul rather than a
469    // broadcast batched matmul that re-reads the packed weight per batch.
470    if let DType::QFloat(_) = weight.dtype() {
471        let dims = input.dims();
472        let analysis = MatmulTransformAnalysis::from_shapes(&input.shape(), &weight.shape());
473
474        let output = match MatmulTransformPolicy::default().action(&analysis) {
475            MatmulTransformAction::MergeBatches { rows } => {
476                let d_in = dims[D - 1];
477                let d_out = weight.dims()[1];
478
479                let folded = input.reshape([rows, d_in]).matmul(weight);
480
481                let mut out_dims = dims;
482                out_dims[D - 1] = d_out;
483                folded.reshape(out_dims)
484            }
485            MatmulTransformAction::Keep => input.matmul(weight.unsqueeze::<D>()),
486        };
487
488        return match bias {
489            Some(bias) => output + bias.unsqueeze(),
490            None => output,
491        };
492    }
493
494    Tensor::new(linear_impl(
495        input.primitive,
496        weight.primitive,
497        bias.map(|b| b.primitive),
498    ))
499}
500
501fn linear_impl(
502    input: BridgeTensor,
503    weight: BridgeTensor,
504    bias: Option<BridgeTensor>,
505) -> BridgeTensor {
506    BridgeTensor::float(Dispatch::linear(
507        input.into_float(),
508        weight.into_float(),
509        bias.map(|b| b.into_float()),
510    ))
511}
512
513/// Computes scaled dot-product attention: softmax(QKᵗ * scale) · V,
514/// where scale defaults to 1/sqrt(head_dim) (configurable via `options.scale`).
515/// Optionally applies masking, additive bias, causal masking, and softcap.
516///
517/// # Arguments
518/// - `query`: Query tensor of shape `[batch_size, num_heads, seq_len_q, head_dim]`
519/// - `key`: Key tensor of shape `[batch_size, num_heads, seq_len_k, head_dim]`
520/// - `value`: Value tensor of shape `[batch_size, num_heads, seq_len_k, val_dim]`
521/// - `mask`: Optional boolean mask of shape `[batch_size, num_heads, seq_len_q, seq_len_k]`,
522///   where `true` indicates positions to mask (i.e. set to -inf before softmax).
523/// - `attn_bias`: Optional float tensor of shape `[batch_size, num_heads, seq_len_q, seq_len_k]`
524///   added to the attention scores before softmax (e.g. ALiBi, relative position biases).
525/// - `options`: Additional attention options (custom scale, softcap, causal masking).
526///
527/// # Returns
528/// A tensor of shape `[batch_size, num_heads, seq_len_q, val_dim]`
529/// representing the attended context per head.
530///
531/// # Note
532/// This implementation does not support dropout and is intended for inference or
533/// use cases where dropout is not needed.
534pub fn attention(
535    query: Tensor<4>,
536    key: Tensor<4>,
537    value: Tensor<4>,
538    mask: Option<Tensor<4, Bool>>,
539    attn_bias: Option<Tensor<4>>,
540    options: AttentionModuleOptions,
541) -> Tensor<4> {
542    Tensor::new(BridgeTensor::float(Dispatch::attention(
543        query.primitive.into_float(),
544        key.primitive.into_float(),
545        value.primitive.into_float(),
546        mask.map(|mask| mask.primitive.into()),
547        attn_bias.map(|bias| bias.primitive.into_float()),
548        options,
549    )))
550}
551
552/// Exports attention fallback to test backend's attention against.
553pub fn attention_fallback(
554    query: Tensor<4>,
555    key: Tensor<4>,
556    value: Tensor<4>,
557    mask: Option<Tensor<4, Bool>>,
558    attn_bias: Option<Tensor<4>>,
559    options: AttentionModuleOptions,
560) -> Tensor<4> {
561    Tensor::new(BridgeTensor::float(
562        burn_backend::ops::attention::attention_fallback::<Dispatch>(
563            query.primitive.into_float(),
564            key.primitive.into_float(),
565            value.primitive.into_float(),
566            mask.map(|mask| mask.primitive.into()),
567            attn_bias.map(|bias| bias.primitive.into_float()),
568            options,
569        ),
570    ))
571}
572
573/// Calculate the [2D convolution](burn_backend::ops::ModuleOps::conv2d) backward pass, returning the gradient for `weight`.
574pub fn conv2d_weight_backward(
575    x: Tensor<4>,
576    weight: Tensor<4>,
577    output_grad: Tensor<4>,
578    options: ConvOptions<2>,
579) -> Tensor<4> {
580    Tensor::new(BridgeTensor::float(Dispatch::conv2d_weight_backward(
581        x.primitive.into_float(),
582        weight.primitive.into_float(),
583        output_grad.primitive.into_float(),
584        options,
585    )))
586}
587
588/// Backward pass for the [avg pooling 2d](ModuleOps::avg_pool2d) operation.
589pub fn avg_pool2d_backward(
590    x: Tensor<4>,
591    grad: Tensor<4>,
592    kernel_size: [usize; 2],
593    stride: [usize; 2],
594    padding: [usize; 2],
595    count_include_pad: bool,
596    ceil_mode: bool,
597) -> Tensor<4> {
598    Tensor::new(BridgeTensor::float(Dispatch::avg_pool2d_backward(
599        x.primitive.into_float(),
600        grad.primitive.into_float(),
601        kernel_size,
602        stride,
603        padding,
604        count_include_pad,
605        ceil_mode,
606    )))
607}
608
609/// Backward pass for the [max pooling 2d](ModuleOps::max_pool2d_with_indices) operation.
610#[allow(clippy::too_many_arguments)]
611pub fn max_pool2d_with_indices_backward(
612    x: Tensor<4>,
613    kernel_size: [usize; 2],
614    stride: [usize; 2],
615    padding: [usize; 2],
616    dilation: [usize; 2],
617    ceil_mode: bool,
618    output_grad: Tensor<4>,
619    indices: Tensor<4, Int>,
620) -> Tensor<4> {
621    Tensor::new(BridgeTensor::float(
622        Dispatch::max_pool2d_with_indices_backward(
623            x.primitive.into_float(),
624            kernel_size,
625            stride,
626            padding,
627            dilation,
628            ceil_mode,
629            output_grad.primitive.into_float(),
630            indices.primitive.into(),
631        )
632        .x_grad,
633    ))
634}
635
636/// Applies Layer Normalization over the last dimension of the input tensor.
637///
638/// Computes `(x - mean) / sqrt(var + epsilon) * gamma + beta`, where `mean` and
639/// (biased) `var` are reduced over the last axis.
640///
641/// # Shapes
642///
643/// - input: `[..., any, d_model]`
644/// - output: `[..., any, d_model]`
645pub fn layer_norm<const D: usize>(
646    input: Tensor<D>,
647    gamma: Tensor<1>,
648    beta: Option<Tensor<1>>,
649    epsilon: f64,
650) -> Tensor<D> {
651    Tensor::new(layer_norm_impl(
652        input.primitive,
653        gamma.primitive,
654        beta.map(|b| b.primitive),
655        epsilon,
656    ))
657}
658
659fn layer_norm_impl(
660    input: BridgeTensor,
661    gamma: BridgeTensor,
662    beta: Option<BridgeTensor>,
663    epsilon: f64,
664) -> BridgeTensor {
665    BridgeTensor::float(Dispatch::layer_norm(
666        input.into_float(),
667        gamma.into_float(),
668        beta.map(|b| b.into_float()),
669        epsilon,
670    ))
671}