# SPDX-License-Identifier: MIT # Copyright (C) 2024-2026, Advanced Micro Devices, Inc. All rights reserved. import torch import triton from typing import Optional from .._triton_kernels.normalization.norm import ( _layernorm_kernel, _fused_add_layernorm_kernel, _quant_layernorm_kernel, _quant_fused_add_layernorm_kernel, _layernorm_bwd_dx_fused_triton, _layernorm_bwd_dwdb_triton, _layernorm_bwd_dwdb_triton_v2, ) from ..utils.types import get_dtype_max from ..utils.logger import AiterTritonLogger _LOGGER = AiterTritonLogger() def _layernorm_forward( y: torch.Tensor, x: torch.Tensor, weight: torch.Tensor, bias: torch.Tensor, mean: torch.Tensor, rstd: torch.Tensor, eps: float = 1e-5, ): M, N = x.shape # Less than 64KB per feature: enqueue fused kernel MAX_FUSED_SIZE = 65536 // x.element_size() BLOCK_SIZE = min(MAX_FUSED_SIZE, triton.next_power_of_2(N)) _layernorm_kernel[(M,)]( x, y, weight, bias, mean, rstd, x.stride(0), y.stride(0), M, N, eps, BLOCK_SIZE ) return def _layernorm_forward_with_add( y: torch.Tensor, x: torch.Tensor, res_in: torch.Tensor, res_out: torch.Tensor, weight: torch.Tensor, bias: torch.Tensor, mean: torch.Tensor, rstd: torch.Tensor, epsilon: float, ): M, N = x.shape # Less than 64KB per feature: enqueue fused kernel MAX_FUSED_SIZE = 65536 // x.element_size() BLOCK_SIZE = min(MAX_FUSED_SIZE, triton.next_power_of_2(N)) _fused_add_layernorm_kernel[(M,)]( x, y, res_in, res_out, weight, bias, mean, rstd, x.stride(0), y.stride(0), M, N, epsilon, BLOCK_SIZE, ) return def _layernorm_backward( dy: torch.Tensor, dx: torch.Tensor, dw: torch.Tensor, db: torch.Tensor, x: torch.Tensor, gamma: torch.Tensor, mu: torch.Tensor, rsigma: torch.Tensor, ): M, N = x.shape # calculate dw and db separately when M is small IGNORE_DW_DB_IN_FUSED = M <= 512 tile_num = max(min(256, M // 4), 1) if M <= 512 and M * N < 64 * 1024 * 1024: tile_num = M elif M >= 8192: tile_num = 2048 max_fused_size = 32768 // x.element_size() next_power = triton.next_power_of_2(N) BLOCK_SIZE = min(max_fused_size, next_power) # For cases with small M and large N, decrease block size to help with occupancy and register spill if tile_num == M: if tile_num > 256: BLOCK_SIZE = min(BLOCK_SIZE, 2048) else: BLOCK_SIZE = min(BLOCK_SIZE, 4096) USE_BLOCKED = N > BLOCK_SIZE num_warps = min(max(BLOCK_SIZE // 256, 1), 8) if not IGNORE_DW_DB_IN_FUSED: _dw = torch.zeros((tile_num, N), dtype=torch.float32, device=gamma.device) _db = torch.zeros((tile_num, N), dtype=torch.float32, device=gamma.device) else: _dw = None _db = None grid_bwd = (tile_num,) _layernorm_bwd_dx_fused_triton[grid_bwd]( dx, dy, _dw, _db, x, gamma, mu, rsigma, x.stride(0), N, NUM_ROWS=M, BLOCK_SIZE_N=BLOCK_SIZE, USE_BLOCKED=USE_BLOCKED, num_warps=num_warps, IGNORE_DW_DB=IGNORE_DW_DB_IN_FUSED, ) grid_reduce = lambda meta: (triton.cdiv(N, meta["BLOCK_SIZE_N"]),) # noqa: E731 if not IGNORE_DW_DB_IN_FUSED: dwdb_block_n = max(16, N // 256) dwdb_block_n = triton.next_power_of_2(dwdb_block_n) dwdb_block_m = (64 * 128) // dwdb_block_n dwdb_block_m = min(triton.next_power_of_2(tile_num), dwdb_block_m) _layernorm_bwd_dwdb_triton[grid_reduce]( _dw, _db, dw, db, min(tile_num, M), N, BLOCK_SIZE_M=dwdb_block_m, BLOCK_SIZE_N=dwdb_block_n, ) else: dwdb_block_n = max(16, N // 256) dwdb_block_n = triton.next_power_of_2(dwdb_block_n) dwdb_block_m = (64 * 128) // dwdb_block_n dwdb_block_m = min(triton.next_power_of_2(M), dwdb_block_m) _layernorm_bwd_dwdb_triton_v2[grid_reduce]( x, dy, mu, rsigma, x.stride(0), dw, db, M, N, BLOCK_SIZE_M=dwdb_block_m, BLOCK_SIZE_N=dwdb_block_n, ) return dx, dw, db class _LayerNorm(torch.autograd.Function): @staticmethod def forward(ctx, x, weight, bias, eps, is_grad_enabled): is_grad = is_grad_enabled and any( tensor.requires_grad for tensor in [x, weight, bias] ) y = torch.empty_like(x) M = x.shape[0] mean = torch.empty((M,), dtype=torch.float32, device=x.device) rstd = torch.empty((M,), dtype=torch.float32, device=x.device) _layernorm_forward(y, x, weight, bias, mean, rstd, eps) if is_grad: ctx.save_for_backward(x, weight, mean, rstd) return y @staticmethod def backward(ctx, dy): x, w, m, v = ctx.saved_tensors N = w.shape[0] dw = torch.empty((N,), dtype=w.dtype, device=w.device) db = torch.empty((N,), dtype=w.dtype, device=w.device) dx = torch.empty_like(dy) _layernorm_backward(dy, dx, dw, db, x, w, m, v) return dx, dw, db, None, None class _Layernorm2dFwdWithAdd(torch.autograd.Function): @staticmethod def forward(ctx, y, x, res_in, res_out, weight, bias, eps, is_grad_enabled): is_grad = is_grad_enabled and any( tensor.requires_grad for tensor in [x, weight, bias] ) M = x.shape[0] mean = torch.empty((M,), dtype=torch.float32, device=x.device) rstd = torch.empty((M,), dtype=torch.float32, device=x.device) _layernorm_forward_with_add( y, x, res_in, res_out, weight, bias, mean, rstd, eps ) if is_grad: ctx.save_for_backward(res_out, weight, mean, rstd) return y @staticmethod def backward(ctx, dy): x, w, m, v = ctx.saved_tensors N = w.shape[0] dw = torch.empty((N,), dtype=w.dtype, device=w.device) db = torch.empty((N,), dtype=w.dtype, device=w.device) dx = torch.empty_like(dy) _layernorm_backward(dy, dx, dw, db, x, w, m, v) return None, dx, None, None, dw, db, None, None def layer_norm( input: torch.Tensor, weight: torch.Tensor, bias: torch.Tensor, eps: float = 1e-5, x_bias: Optional[torch.Tensor] = None, ) -> torch.Tensor: """ Applies Layer Normalization over a mini-batch of inputs. Key parameters: - input: The input tensor to be normalized with shape (M, N). - weight: The learnable weights tensor with shape (N, ). - bias: The learnable bias tensor with shape (N, ) - eps: A value added to the denominator for numerical stability. Returns: - Output: The output tensor with shape (M, N). """ _LOGGER.info(f"LAYERNORM: input={tuple(input.shape)} weight={tuple(weight.shape)} ") return _LayerNorm.apply(input, weight, bias, eps, torch.is_grad_enabled()) def layernorm2d_fwd_with_add( out: torch.Tensor, input: torch.Tensor, residual_in: torch.Tensor, residual_out: torch.Tensor, weight: torch.Tensor, bias: torch.Tensor, epsilon: float, x_bias: Optional[torch.Tensor] = None, ) -> torch.Tensor: """ Adds two inputs and then applies Layer Normalization Key parameters: - out: The output of layer normalization with shape (M, N). Allocated by the caller - input: The input tensor to be normalized with shape (M, N). - residual_in: Tensor added to the input and same shape as input (M, N) - residual_out: Output tensor that is input + residual_in with shape (M, N). Must be allocated by the caller - weight: The learnable weights tensor with shape (N, ). - bias: Bias added to the result of layer norm with shape (N,) - epsilon: A value added to the denominator for numerical stability. Returns: - out: The output tensor with shape (M, N). - residual_out: Output tensor that is input + residual_in with shape (M, N). """ _LOGGER.info( f"LAYERNORM_2D_FWD_ADD: input={tuple(input.shape)} weight={tuple(weight.shape)} residual_in={tuple(residual_in.shape)} " ) return _Layernorm2dFwdWithAdd.apply( out, input, residual_in, residual_out, weight, bias, epsilon, torch.is_grad_enabled(), ) def layernorm2d_fwd_with_dynamicquant( out: torch.Tensor, input: torch.Tensor, yscale: torch.Tensor, weight: torch.Tensor, bias: torch.Tensor, epsilon: float = 1e-5, x_bias: Optional[torch.Tensor] = None, ) -> torch.Tensor: """ Applies Layer Normalization and then quantizes the output Key parameters: - out: The output of layer normalization with shape (M, N). Allocated by the caller - input: The input tensor to be normalized with shape (M, N) and dtype in (fp32, fp16 or bf16) - yscale: Output scale tensor with shape (M,) and dtype fp32. Allocated by the caller - weight: The learnable weights tensor with shape (N, ). - bias: Bias added to the result of layer norm with shape (N,) - eps: A value added to the denominator for numerical stability. Returns: - out: The output tensor with shape (M, N). - yscale: Output scale tensor with shape (M,). Allocated by the caller """ _LOGGER.info( f"LAYERNORM_2D_FWD_DYNAMICQUANT: input={tuple(input.shape)} weight={tuple(weight.shape)} yscale={tuple(yscale.shape)} " ) M, N = input.shape # Less than 64KB per feature: enqueue fused kernel MAX_FUSED_SIZE = 65536 // input.element_size() BLOCK_SIZE = min(MAX_FUSED_SIZE, triton.next_power_of_2(N)) xscale = None IS_SMOOTH = False DTYPE_MAX = get_dtype_max(out.dtype) # Auxiliary tensor to store the RMSNorm output as fp32 before applying the quantization when using the blocked approach aux = torch.empty(M, N, dtype=torch.float32, device=input.device) _quant_layernorm_kernel[(M,)]( input, out, weight, bias, xscale, yscale, aux, input.stride(0), out.stride(0), aux.stride(0), M, N, epsilon, DTYPE_MAX, IS_SMOOTH, BLOCK_SIZE, ) return def layernorm2d_fwd_with_smoothquant( out: torch.Tensor, input: torch.Tensor, xscale: torch.Tensor, yscale: torch.Tensor, weight: torch.Tensor, bias: torch.Tensor, epsilon: float = 1e-5, x_bias: Optional[torch.Tensor] = None, ) -> torch.Tensor: """ Applies Layer Normalization and then quantizes the output Key parameters: - input: The input tensor to be normalized with shape (M, N). - xscale: Input scale tensor which is multiplied with the output of layer normalization before quantization. - yscale: Output scale tensor with shape (M,) and dtype fp32. Allocated by the caller - weight: The learnable weights tensor with shape (N, ). - bias: Bias added to the result of layer norm with shape (N,) - eps: A value added to the denominator for numerical stability. Returns: - Output: The output tensor with shape (M, N). """ _LOGGER.info( f"RMSNORM_2D_FWD_SMOOTHQUANT: input={tuple(input.shape)} weight={tuple(weight.shape)} xscale={tuple(xscale.shape)} yscale={tuple(yscale.shape)} " ) M, N = input.shape # Less than 64KB per feature: enqueue fused kernel MAX_FUSED_SIZE = 65536 // input.element_size() BLOCK_SIZE = min(MAX_FUSED_SIZE, triton.next_power_of_2(N)) IS_SMOOTH = True DTYPE_MAX = get_dtype_max(out.dtype) # Auxiliary tensor to store the RMSNorm output as fp32 before applying the quantization when using the blocked approach aux = torch.empty(M, N, dtype=torch.float32, device=input.device) _quant_layernorm_kernel[(M,)]( input, out, weight, bias, xscale, yscale, aux, input.stride(0), out.stride(0), aux.stride(0), M, N, epsilon, DTYPE_MAX, IS_SMOOTH, BLOCK_SIZE, ) return def layernorm2d_fwd_with_add_dynamicquant( out: torch.Tensor, input: torch.Tensor, residual_in: torch.Tensor, residual_out: torch.Tensor, yscale: torch.Tensor, weight: torch.Tensor, bias: torch.Tensor, epsilon: float = 1e-5, x_bias: Optional[torch.Tensor] = None, ) -> torch.Tensor: """ Adds two input toegether, then does layer Normalization before quantizing the final output Key parameters: - out: The output of layer normalization with shape (M, N). Allocated by the caller - input: The input tensor to be normalized with shape (M, N) and dtype in (fp32, fp16 or bf16) - residual_in: Tensor added to the input and same shape as input (M, N) - residual_out: Output tensor that is input + residual_in with shape (M, N). Must be allocated by the caller - yscale: Output scale tensor with shape (M,) and dtype fp32. Allocated by the caller - weight: The learnable weights tensor with shape (N, ). - bias: Bias added to the result of layer norm with shape (N,) - eps: A value added to the denominator for numerical stability. Returns: - out: The output tensor with shape (M, N). - yscale: Output scale tensor with shape (M,). Allocated by the caller """ _LOGGER.info( f"LAYERNORM_2D_FWD_ADD_DYNAMICQUANT: input={input.shape} weight={weight.shape} residual_in={residual_in.shape} yscale={yscale.shape} " ) M, N = input.shape # Less than 64KB per feature: enqueue fused kernel MAX_FUSED_SIZE = 65536 // input.element_size() BLOCK_SIZE = min(MAX_FUSED_SIZE, triton.next_power_of_2(N)) xscale = None IS_SMOOTH = False DTYPE_MAX = get_dtype_max(out.dtype) # Auxiliary tensor to store the RMSNorm output as fp32 before applying the quantization when using the blocked approach aux = torch.empty(M, N, dtype=torch.float32, device=input.device) _quant_fused_add_layernorm_kernel[(M,)]( input, out, residual_in, residual_out, weight, bias, xscale, yscale, aux, input.stride(0), out.stride(0), aux.stride(0), M, N, epsilon, DTYPE_MAX, IS_SMOOTH, BLOCK_SIZE, ) return def layernorm2d_fwd_with_add_smoothquant( out: torch.Tensor, input: torch.Tensor, residual_in: torch.Tensor, residual_out: torch.Tensor, xscale: torch.Tensor, yscale: torch.Tensor, weight: torch.Tensor, bias: torch.Tensor, epsilon: float = 1e-5, x_bias: Optional[torch.Tensor] = None, ) -> torch.Tensor: """ Applies Layer Normalization and then quantizes the output Key parameters: - input: The input tensor to be normalized with shape (M, N). - residual_in: Tensor added to the input and same shape as input (M, N) - residual_out: Output tensor that is input + residual_in with shape (M, N). Must be allocated by the caller - xscale: Input scale tensor which is multiplied with the output of layer normalization before quantization. - yscale: Output scale tensor with shape (M,) and dtype fp32. Allocated by the caller - weight: The learnable weights tensor with shape (N, ). - bias: Bias added to the result of layer norm with shape (N,) - eps: A value added to the denominator for numerical stability. Returns: - Output: The output tensor with shape (M, N). - yscale: Output scale tensor with shape (M,). Allocated by the caller """ _LOGGER.info( f"LAYERNORM_2D_FWD_ADD_SMOOTHQUANT: input={tuple(input.shape)} weight={tuple(weight.shape)} " + f"residual_in={tuple(residual_in.shape)} xscale={tuple(xscale.shape)} yscale={tuple(yscale.shape)} " ) M, N = input.shape # Less than 64KB per feature: enqueue fused kernel MAX_FUSED_SIZE = 65536 // input.element_size() BLOCK_SIZE = min(MAX_FUSED_SIZE, triton.next_power_of_2(N)) IS_SMOOTH = True DTYPE_MAX = get_dtype_max(out.dtype) # Auxiliary tensor to store the RMSNorm output as fp32 before applying the quantization when using the blocked approach aux = torch.empty(M, N, dtype=torch.float32, device=input.device) _quant_fused_add_layernorm_kernel[(M,)]( input, out, residual_in, residual_out, weight, bias, xscale, yscale, aux, input.stride(0), out.stride(0), aux.stride(0), M, N, epsilon, DTYPE_MAX, IS_SMOOTH, BLOCK_SIZE, ) return