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 3D to 4D fold, the inverse of [unfold4d].
270///
271/// Combines an array of sliding local blocks into a large containing tensor, summing the
272/// values of blocks that overlap. This is the operation performed by
273/// [`torch.nn.Fold`](https://pytorch.org/docs/stable/generated/torch.nn.Fold.html), and is the
274/// adjoint of [unfold4d]: it reuses the same one-hot kernel through a [conv_transpose2d].
275///
276/// # Arguments
277///
278/// * `x` - Input columns of shape
279///   `[batch_size, channels * kernel_size_0 * kernel_size_1, number_of_blocks]`.
280/// * `output_size` - The spatial size `[height, width]` of the folded output tensor.
281/// * `kernel_size` - The size of the sliding blocks.
282/// * `options` - The stride, padding and dilation of the matching unfold.
283///
284/// # Returns
285///
286/// A tensor of shape `[batch_size, channels, output_size_0, output_size_1]`.
287pub fn fold4d(
288    x: Tensor<3>,
289    output_size: [usize; 2],
290    kernel_size: [usize; 2],
291    options: UnfoldOptions,
292) -> Tensor<4> {
293    Tensor::new(BridgeTensor::float(Dispatch::fold4d(
294        x.primitive.into_float(),
295        output_size,
296        kernel_size,
297        options,
298    )))
299}
300
301/// Applies a [1D max pooling](burn_backend::ops::ModuleOps::max_pool1d).
302pub fn max_pool1d(
303    x: Tensor<3>,
304    kernel_size: usize,
305    stride: usize,
306    padding: usize,
307    dilation: usize,
308    ceil_mode: bool,
309) -> Tensor<3> {
310    Tensor::new(BridgeTensor::float(Dispatch::max_pool1d(
311        x.primitive.into_float(),
312        kernel_size,
313        stride,
314        padding,
315        dilation,
316        ceil_mode,
317    )))
318}
319
320/// Applies a [2D max pooling](burn_backend::ops::ModuleOps::max_pool2d).
321pub fn max_pool2d(
322    x: Tensor<4>,
323    kernel_size: [usize; 2],
324    stride: [usize; 2],
325    padding: [usize; 2],
326    dilation: [usize; 2],
327    ceil_mode: bool,
328) -> Tensor<4> {
329    Tensor::new(BridgeTensor::float(Dispatch::max_pool2d(
330        x.primitive.into_float(),
331        kernel_size,
332        stride,
333        padding,
334        dilation,
335        ceil_mode,
336    )))
337}
338
339/// Applies a [2D avg pooling](burn_backend::ops::ModuleOps::avg_pool2d).
340pub fn avg_pool2d(
341    x: Tensor<4>,
342    kernel_size: [usize; 2],
343    stride: [usize; 2],
344    padding: [usize; 2],
345    count_include_pad: bool,
346    ceil_mode: bool,
347) -> Tensor<4> {
348    Tensor::new(BridgeTensor::float(Dispatch::avg_pool2d(
349        x.primitive.into_float(),
350        kernel_size,
351        stride,
352        padding,
353        count_include_pad,
354        ceil_mode,
355    )))
356}
357
358/// Applies a [1D avg pooling](burn_backend::ops::ModuleOps::avg_pool1d).
359pub fn avg_pool1d(
360    x: Tensor<3>,
361    kernel_size: usize,
362    stride: usize,
363    padding: usize,
364    count_include_pad: bool,
365    ceil_mode: bool,
366) -> Tensor<3> {
367    Tensor::new(BridgeTensor::float(Dispatch::avg_pool1d(
368        x.primitive.into_float(),
369        kernel_size,
370        stride,
371        padding,
372        count_include_pad,
373        ceil_mode,
374    )))
375}
376
377/// Applies a [1D max pooling](burn_backend::ops::ModuleOps::max_pool1d).
378pub fn max_pool1d_with_indices(
379    x: Tensor<3>,
380    kernel_size: usize,
381    stride: usize,
382    padding: usize,
383    dilation: usize,
384    ceil_mode: bool,
385) -> (Tensor<3>, Tensor<3, Int>) {
386    let indices_dtype = x.device().settings().int_dtype;
387    let output = Dispatch::max_pool1d_with_indices(
388        x.primitive.into_float(),
389        kernel_size,
390        stride,
391        padding,
392        dilation,
393        ceil_mode,
394        indices_dtype,
395    );
396
397    (
398        Tensor::new(BridgeTensor::float(output.output)),
399        Tensor::new(BridgeTensor::int(output.indices)),
400    )
401}
402
403/// Applies a [2D max pooling with indices](burn_backend::ops::ModuleOps::max_pool2d_with_indices).
404pub fn max_pool2d_with_indices(
405    x: Tensor<4>,
406    kernel_size: [usize; 2],
407    stride: [usize; 2],
408    padding: [usize; 2],
409    dilation: [usize; 2],
410    ceil_mode: bool,
411) -> (Tensor<4>, Tensor<4, Int>) {
412    let indices_dtype = x.device().settings().int_dtype;
413    let output = Dispatch::max_pool2d_with_indices(
414        x.primitive.into_float(),
415        kernel_size,
416        stride,
417        padding,
418        dilation,
419        ceil_mode,
420        indices_dtype,
421    );
422
423    (
424        Tensor::new(BridgeTensor::float(output.output)),
425        Tensor::new(BridgeTensor::int(output.indices)),
426    )
427}
428
429/// Applies a [2D adaptive avg pooling](burn_backend::ops::ModuleOps::adaptive_avg_pool2d).
430pub fn adaptive_avg_pool2d(x: Tensor<4>, output_size: [usize; 2]) -> Tensor<4> {
431    Tensor::new(BridgeTensor::float(Dispatch::adaptive_avg_pool2d(
432        x.primitive.into_float(),
433        output_size,
434    )))
435}
436
437/// Applies a [3D adaptive avg pooling](burn_backend::ops::ModuleOps::adaptive_avg_pool3d).
438pub fn adaptive_avg_pool3d(x: Tensor<5>, output_size: [usize; 3]) -> Tensor<5> {
439    Tensor::new(BridgeTensor::float(Dispatch::adaptive_avg_pool3d(
440        x.primitive.into_float(),
441        output_size,
442    )))
443}
444
445/// Applies a [1D adaptive avg pooling](burn_backend::ops::ModuleOps::adaptive_avg_pool1d).
446pub fn adaptive_avg_pool1d(x: Tensor<3>, output_size: usize) -> Tensor<3> {
447    Tensor::new(BridgeTensor::float(Dispatch::adaptive_avg_pool1d(
448        x.primitive.into_float(),
449        output_size,
450    )))
451}
452
453/// Applies a [2D interpolation](burn_backend::ops::ModuleOps::interpolate).
454pub fn interpolate(
455    x: Tensor<4>,
456    output_size: [usize; 2],
457    options: InterpolateOptions,
458) -> Tensor<4> {
459    Tensor::new(BridgeTensor::float(Dispatch::interpolate(
460        x.primitive.into_float(),
461        output_size,
462        options,
463    )))
464}
465
466/// Applies a linear transformation to the input tensor using the given weight and bias.
467///
468/// ```math
469/// y = x @ weight + [bias]
470/// ```
471///
472/// # Arguments:
473///
474/// - `input` is the input tensor, ``[..., d_input]``.
475/// - `weight` is the weight tensor, ``[d_input, d_output]``.
476/// - `bias` is the bias tensor (optional), ``[d_output]``.
477///
478/// # Returns:
479///
480/// The transformed tensor, ``[..., d_output]``.
481///
482/// # Compatibility
483///
484/// This function differs from PyTorch's ``torch.nn.functional.linear`` in that it does not
485/// transpose the weight matrix. In PyTorch, the weight matrix is transposed before
486/// multiplication:
487///
488/// ```math
489/// y = x @ weight^T + [bias]
490/// ```
491pub fn linear<const D: usize>(
492    input: Tensor<D>,
493    weight: Tensor<2>,
494    bias: Option<Tensor<1>>,
495) -> Tensor<D> {
496    if D == 1 {
497        // Insert and remove an extra batch dimension for the batch matmul to work.
498        let input = input.unsqueeze::<2>();
499        let output = linear(input, weight, bias);
500        return output.squeeze_dim(0);
501    }
502
503    // A quantized weight must stay quantized: `linear_impl` converts its
504    // operands to float, which would dequantize (materialize) the whole weight
505    // matrix on every forward. Route through the quantized matmul instead, which
506    // streams the packed weight directly — but reuse the same batch-fold policy
507    // the float `linear` applies, so a decode-shaped call folds its batches into
508    // the rows for one `[rows, d_in] @ [d_in, d_out]` matmul rather than a
509    // broadcast batched matmul that re-reads the packed weight per batch.
510    if let DType::QFloat(_) = weight.dtype() {
511        let dims = input.dims();
512        let analysis = MatmulTransformAnalysis::from_shapes(&input.shape(), &weight.shape());
513
514        let output = match MatmulTransformPolicy::default().action(&analysis) {
515            MatmulTransformAction::MergeBatches { rows } => {
516                let d_in = dims[D - 1];
517                let d_out = weight.dims()[1];
518
519                let folded = input.reshape([rows, d_in]).matmul(weight);
520
521                let mut out_dims = dims;
522                out_dims[D - 1] = d_out;
523                folded.reshape(out_dims)
524            }
525            MatmulTransformAction::Keep => input.matmul(weight.unsqueeze::<D>()),
526        };
527
528        return match bias {
529            Some(bias) => output + bias.unsqueeze(),
530            None => output,
531        };
532    }
533
534    Tensor::new(linear_impl(
535        input.primitive,
536        weight.primitive,
537        bias.map(|b| b.primitive),
538    ))
539}
540
541fn linear_impl(
542    input: BridgeTensor,
543    weight: BridgeTensor,
544    bias: Option<BridgeTensor>,
545) -> BridgeTensor {
546    BridgeTensor::float(Dispatch::linear(
547        input.into_float(),
548        weight.into_float(),
549        bias.map(|b| b.into_float()),
550    ))
551}
552
553/// Computes scaled dot-product attention: softmax(QKᵗ * scale) · V,
554/// where scale defaults to 1/sqrt(head_dim) (configurable via `options.scale`).
555/// Optionally applies masking, additive bias, causal masking, and softcap.
556///
557/// # Arguments
558/// - `query`: Query tensor of shape `[batch_size, num_heads, seq_len_q, head_dim]`
559/// - `key`: Key tensor of shape `[batch_size, num_heads, seq_len_k, head_dim]`
560/// - `value`: Value tensor of shape `[batch_size, num_heads, seq_len_k, val_dim]`
561/// - `mask`: Optional boolean mask of shape `[batch_size, num_heads, seq_len_q, seq_len_k]`,
562///   where `true` indicates positions to mask (i.e. set to -inf before softmax).
563/// - `attn_bias`: Optional float tensor of shape `[batch_size, num_heads, seq_len_q, seq_len_k]`
564///   added to the attention scores before softmax (e.g. ALiBi, relative position biases).
565/// - `options`: Additional attention options (custom scale, softcap, causal masking).
566///
567/// # Returns
568/// A tensor of shape `[batch_size, num_heads, seq_len_q, val_dim]`
569/// representing the attended context per head.
570///
571/// # Note
572/// This implementation does not support dropout and is intended for inference or
573/// use cases where dropout is not needed.
574pub fn attention(
575    query: Tensor<4>,
576    key: Tensor<4>,
577    value: Tensor<4>,
578    mask: Option<Tensor<4, Bool>>,
579    attn_bias: Option<Tensor<4>>,
580    options: AttentionModuleOptions,
581) -> Tensor<4> {
582    Tensor::new(BridgeTensor::float(Dispatch::attention(
583        query.primitive.into_float(),
584        key.primitive.into_float(),
585        value.primitive.into_float(),
586        mask.map(|mask| mask.primitive.into()),
587        attn_bias.map(|bias| bias.primitive.into_float()),
588        options,
589    )))
590}
591
592/// Exports attention fallback to test backend's attention against.
593pub fn attention_fallback(
594    query: Tensor<4>,
595    key: Tensor<4>,
596    value: Tensor<4>,
597    mask: Option<Tensor<4, Bool>>,
598    attn_bias: Option<Tensor<4>>,
599    options: AttentionModuleOptions,
600) -> Tensor<4> {
601    Tensor::new(BridgeTensor::float(
602        burn_backend::ops::attention::attention_fallback::<Dispatch>(
603            query.primitive.into_float(),
604            key.primitive.into_float(),
605            value.primitive.into_float(),
606            mask.map(|mask| mask.primitive.into()),
607            attn_bias.map(|bias| bias.primitive.into_float()),
608            options,
609        ),
610    ))
611}
612
613/// Calculate the [2D convolution](burn_backend::ops::ModuleOps::conv2d) backward pass, returning the gradient for `weight`.
614pub fn conv2d_weight_backward(
615    x: Tensor<4>,
616    weight: Tensor<4>,
617    output_grad: Tensor<4>,
618    options: ConvOptions<2>,
619) -> Tensor<4> {
620    Tensor::new(BridgeTensor::float(Dispatch::conv2d_weight_backward(
621        x.primitive.into_float(),
622        weight.primitive.into_float(),
623        output_grad.primitive.into_float(),
624        options,
625    )))
626}
627
628/// Backward pass for the [avg pooling 2d](ModuleOps::avg_pool2d) operation.
629pub fn avg_pool2d_backward(
630    x: Tensor<4>,
631    grad: Tensor<4>,
632    kernel_size: [usize; 2],
633    stride: [usize; 2],
634    padding: [usize; 2],
635    count_include_pad: bool,
636    ceil_mode: bool,
637) -> Tensor<4> {
638    Tensor::new(BridgeTensor::float(Dispatch::avg_pool2d_backward(
639        x.primitive.into_float(),
640        grad.primitive.into_float(),
641        kernel_size,
642        stride,
643        padding,
644        count_include_pad,
645        ceil_mode,
646    )))
647}
648
649/// Backward pass for the [max pooling 2d](ModuleOps::max_pool2d_with_indices) operation.
650#[allow(clippy::too_many_arguments)]
651pub fn max_pool2d_with_indices_backward(
652    x: Tensor<4>,
653    kernel_size: [usize; 2],
654    stride: [usize; 2],
655    padding: [usize; 2],
656    dilation: [usize; 2],
657    ceil_mode: bool,
658    output_grad: Tensor<4>,
659    indices: Tensor<4, Int>,
660) -> Tensor<4> {
661    Tensor::new(BridgeTensor::float(
662        Dispatch::max_pool2d_with_indices_backward(
663            x.primitive.into_float(),
664            kernel_size,
665            stride,
666            padding,
667            dilation,
668            ceil_mode,
669            output_grad.primitive.into_float(),
670            indices.primitive.into(),
671        )
672        .x_grad,
673    ))
674}
675
676/// Applies Layer Normalization over the last dimension of the input tensor.
677///
678/// Computes `(x - mean) / sqrt(var + epsilon) * gamma + beta`, where `mean` and
679/// (biased) `var` are reduced over the last axis.
680///
681/// # Shapes
682///
683/// - input: `[..., any, d_model]`
684/// - output: `[..., any, d_model]`
685pub fn layer_norm<const D: usize>(
686    input: Tensor<D>,
687    gamma: Tensor<1>,
688    beta: Option<Tensor<1>>,
689    epsilon: f64,
690) -> Tensor<D> {
691    Tensor::new(layer_norm_impl(
692        input.primitive,
693        gamma.primitive,
694        beta.map(|b| b.primitive),
695        epsilon,
696    ))
697}
698
699fn layer_norm_impl(
700    input: BridgeTensor,
701    gamma: BridgeTensor,
702    beta: Option<BridgeTensor>,
703    epsilon: f64,
704) -> BridgeTensor {
705    BridgeTensor::float(Dispatch::layer_norm(
706        input.into_float(),
707        gamma.into_float(),
708        beta.map(|b| b.into_float()),
709        epsilon,
710    ))
711}