# Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # pylint: disable=unused-argument, redefined-builtin, invalid-name """Gradient definitions for Relax operators.""" import functools import operator from tvm import relax from tvm.arith import Analyzer from tvm.ir import Call, PrimType from tvm.relax.type import ShapeType from ..block_builder import BlockBuilder from ..expr import Expr, ShapeExpr, Var from .base import register_gradient from .binary import greater_equal, less from .create import triu from .datatype import astype from .grad import ( avg_pool2d_backward, max_pool2d_backward, nll_loss_backward, no_grad, take_backward, ) from .index import strided_slice from .linear_algebra import matmul from .manipulate import ( broadcast_to, collapse_sum_to, concat, expand_dims, flatten, permute_dims, reshape, split, squeeze, ) from .nn import conv2d, conv2d_transpose from .search import where from .statistical import cumsum, sum from .unary import cos, exp, log, sigmoid, sin # TODO(yixin, chaofan): handle symbolic shape for most of the gradients ##################### Utilities ##################### def _get_shape(expr: Expr) -> ShapeExpr: """Get the shape from a Tensor expr.""" try: shape = expr.ty.shape except Exception as error: raise RuntimeError( f"Get the shape of {expr} failed. Please normalize it first and ensure it is a Tensor." ) from error return shape def _get_dtype(expr: Expr) -> str: """Get the dtype from a Tensor expr.""" try: dtype = expr.ty.dtype except Exception as error: raise RuntimeError( f"Get the dtype of {expr} failed. Please normalize it first and ensure it is a Tensor." ) from error if isinstance(dtype, PrimType): dtype = dtype.dtype return dtype def _fit_shape(bb: BlockBuilder, input_grad: Expr, input: Expr) -> Expr: """When expr and target has the same shape, return expr; otherwise return `collapse_sum_to(expr, target.ty.shape)`. Will use BlockBuilder to normalize expr first. """ target_shape = _get_shape(input) expr_ty = _get_shape(bb.normalize(input_grad)).ty target_ty = target_shape.ty assert isinstance(expr_ty, ShapeType) assert isinstance(target_ty, ShapeType) def _check_shape_equal(lhs: ShapeType, rhs: ShapeType): if len(lhs.values) != len(rhs.values): return False analyzer = Analyzer() for i, field in enumerate(lhs.values): if not analyzer.can_prove_equal(field, rhs.values[i]): return False return True return ( input_grad if _check_shape_equal(expr_ty, target_ty) else collapse_sum_to(input_grad, target_shape) ) def _get_shape_prod(expr, axis): # Requires static shape shape = _get_shape(expr) if axis is None: return functools.reduce(operator.mul, (int(i) for i in shape), 1) return functools.reduce(operator.mul, (int(shape[int(i)]) for i in axis), 1) ##################### Binary ##################### @register_gradient("relax.add") def add_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of add. Forward Form: `z = relax.add(x, y)` Backward: Returns `[z_output_grad, z_grad]`. """ return [ _fit_shape(ctx, output_grad, orig_call.args[0]), _fit_shape(ctx, output_grad, orig_call.args[1]), ] @register_gradient("relax.subtract") def subtract_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of subtract. Forward Form: `z = relax.subtract(x, y)` Backward: Returns `[z_output_grad, -z_grad]`. """ return [ _fit_shape(ctx, output_grad, orig_call.args[0]), _fit_shape(ctx, -output_grad, orig_call.args[1]), ] @register_gradient("relax.multiply") def multiply_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of multiply. Forward Form: `z = relax.multiply(x, y)` Backward: Returns `[z_grad * y, z_grad * x]`. """ x, y = orig_call.args return [ _fit_shape(ctx, output_grad * y, x), _fit_shape(ctx, output_grad * x, y), ] @register_gradient("relax.divide") def divide_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of divide. Forward Form: `z = relax.divide(x, y)` Backward: Returns `[z_grad / y, -z_grad * z / y]`. """ x, y = orig_call.args return [ _fit_shape(ctx, output_grad / y, x), _fit_shape(ctx, -output_grad * orig_var / y, y), ] @register_gradient("relax.power") def power_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of power. Forward Form: `z = relax.power(x, y)` Backward: Returns `[y * x ** (y-1) * z_grad, z * ln(x) * z_grad]`. The gradient w.r.t. the second parameter, y, makes sense only when x > 0. """ x, y = orig_call.args one = relax.const(1, _get_dtype(y)) return [ _fit_shape(ctx, output_grad * y * (x ** (y - one)), x), _fit_shape(ctx, output_grad * orig_var * log(x), y), ] @register_gradient("relax.maximum") def maximum_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of maximum. Forward Form: `z = relax.maximum(x, y)` Backward: Returns `[where(x < y, 0, z_grad), where(x >= y, 0, z_grad)]`. """ x = orig_call.args[0] y = orig_call.args[1] zero = relax.const(0, _get_dtype(x)) return [ _fit_shape(ctx, where(less(x, y), zero, output_grad), x), _fit_shape(ctx, where(greater_equal(x, y), zero, output_grad), y), ] @register_gradient("relax.minimum") def minimum_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of minimum. Forward Form: `z = relax.minimum(x, y)` Backward: Returns `[where(x >= y, 0, z_grad), where(x < y, 0, z_grad)]`. """ x = orig_call.args[0] y = orig_call.args[1] zero = relax.const(0, _get_dtype(x)) return [ _fit_shape(ctx, where(greater_equal(x, y), zero, output_grad), x), _fit_shape(ctx, where(less(x, y), zero, output_grad), y), ] ##################### Binary Comparison ##################### # For comparison operators, the gradients are no_grad @register_gradient("relax.equal") def equal_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: return [no_grad(orig_call.args[0]), no_grad(orig_call.args[1])] @register_gradient("relax.greater") def greater_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: return [no_grad(orig_call.args[0]), no_grad(orig_call.args[1])] @register_gradient("relax.greater_equal") def greater_equal_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: return [no_grad(orig_call.args[0]), no_grad(orig_call.args[1])] @register_gradient("relax.less") def less_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: return [no_grad(orig_call.args[0]), no_grad(orig_call.args[1])] @register_gradient("relax.less_equal") def less_equal_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: return [no_grad(orig_call.args[0]), no_grad(orig_call.args[1])] @register_gradient("relax.not_equal") def not_equal_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: return [no_grad(orig_call.args[0]), no_grad(orig_call.args[1])] ##################### Create ##################### # For zeros/ones/full operators, the gradients are no_grad. @register_gradient("relax.zeros_like") def zeros_like_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: return [no_grad(orig_call.args[0])] @register_gradient("relax.ones_like") def ones_like_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: return [no_grad(orig_call.args[0])] @register_gradient("relax.full_like") def full_like_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: return [no_grad(orig_call.args[0]), no_grad(orig_call.args[1])] @register_gradient("relax.zeros") def zeros_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: return [no_grad(orig_call.args[0])] @register_gradient("relax.ones") def ones_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: return [no_grad(orig_call.args[0])] @register_gradient("relax.full") def full_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: return [no_grad(orig_call.args[0]), no_grad(orig_call.args[1])] # Other create gradients operators @register_gradient("relax.triu") def triu_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of triu. Forward Form: `y = relax.triu(x, k)` Backward: Returns `[triu(y_grad, k)]`. """ k = orig_call.args[1] return [triu(output_grad, k)] ##################### Unary ##################### @register_gradient("relax.abs") def abs_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of abs. Forward Form: `y = relax.abs(x)` Backward: Returns `[y_grad * where(x < 0, -1, 1)]`. """ x = orig_call.args[0] zero = relax.const(0, _get_dtype(x)) one = relax.const(1, _get_dtype(x)) return [output_grad * where(less(x, zero), -one, one)] @register_gradient("relax.cos") def cos_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of cos. Forward Form: `y = relax.cos(x)` Backward: Returns `[-y_grad * sin(x)]`. """ return [-output_grad * sin(orig_call.args[0])] @register_gradient("relax.exp") def exp_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of exp. Forward Form: `y = relax.exp(x)` Backward: Returns `[y_grad * y]`. """ return [output_grad * orig_var] @register_gradient("relax.log") def log_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of log. Forward Form: `y = relax.log(x)` Backward: Returns `[y_grad / x]`. """ return [output_grad / orig_call.args[0]] @register_gradient("relax.negative") def negative_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of negative. Forward Form: `y = relax.negative(x)` Backward: Returns `[-y_grad]`. """ return [-output_grad] @register_gradient("relax.sigmoid") def sigmoid_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of sigmoid. Forward Form: `y = relax.sigmoid(x)` Backward: Returns `[y_grad * y * (1 - y)]`. """ one = relax.const(1, _get_dtype(orig_call.args[0])) return [output_grad * orig_var * (one - orig_var)] @register_gradient("relax.sin") def sin_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of sin. Forward Form: `y = relax.sin(x)` Backward: Returns `[y_grad * cos(x)]`. """ return [output_grad * cos(orig_call.args[0])] @register_gradient("relax.sqrt") def sqrt_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of sqrt. Forward Form: `y = relax.sqrt(x)` Backward: Returns `[0.5 * y_grad / y]`. """ x = orig_call.args[0] cst = relax.const(0.5, _get_dtype(x)) return [cst * output_grad / orig_var] @register_gradient("relax.tanh") def tanh_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of tanh. Forward Form: `y = relax.tanh(x)` Backward: Returns `[y_grad * (1 - y * y)]`. """ one = relax.const(1, _get_dtype(orig_call.args[0])) return [output_grad * (one - orig_var * orig_var)] ##################### Statistical ##################### @register_gradient("relax.sum") def sum_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of sum. Forward Form: `y = relax.sum(x, axis, keepdims)` Backward: Returns `[broadcast_to(y_output_grad, x.shape)]`. If `keepdims=False`, the summed axis will be added back. """ axis = orig_call.attrs.axis keepdims = orig_call.attrs.keepdims if not keepdims and axis: output_grad = expand_dims(output_grad, axis) return [broadcast_to(output_grad, _get_shape(orig_call.args[0]))] @register_gradient("relax.mean") def mean_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of mean. Forward Form: `y = relax.mean(x, axis, keepdims)` Backward: Returns `[broadcast_to(y_output_grad, x.shape) / prod(x.shape[i] for i in axis)]`. If `keepdims=False`, the mean axis will be added back. """ axis = orig_call.attrs.axis keepdims = orig_call.attrs.keepdims output_grad = output_grad / relax.const( _get_shape_prod(orig_call.args[0], axis), _get_dtype(output_grad) ) if not keepdims and axis: output_grad = expand_dims(output_grad, axis) return [broadcast_to(output_grad, _get_shape(orig_call.args[0]))] @register_gradient("relax.variance") def variance_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of variance. Forward Form: `y = relax.variance(x, axis, keepdims)` Backward: Returns `[broadcast_to(y_output_grad, x.shape)]`. If `keepdims=False`, the summed axis will be added back. """ x = orig_call.args[0] axis = orig_call.attrs.axis keepdims = orig_call.attrs.keepdims shape_prod = _get_shape_prod(x, axis) dtype = _get_dtype(x) grad1 = relax.const(2.0 / shape_prod, dtype) * x grad2 = relax.const(2.0 / shape_prod / shape_prod, dtype) * sum(x, axis, keepdims=True) if not keepdims and axis: output_grad = expand_dims(output_grad, axis) return [output_grad * (grad1 - grad2)] ##################### Manipulate ##################### @register_gradient("relax.permute_dims") def permute_dims_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of permute_dims. Forward Form: `y = relax.permute_dims(x, axes)` Backward: Returns grad transposed over the **inverse permutation** of the original permute_dims axes. """ axes = orig_call.attrs.axes if axes: dims = len(axes) new_axes = [0] * dims for i in range(dims): new_axes[int(axes[i])] = i return [permute_dims(output_grad, axes=new_axes)] return [permute_dims(output_grad)] @register_gradient("relax.concat") def concat_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of concat. Forward Form: `y = relax.concat((x1, x2, x3), axis)` Backward: Returns `[split(y_output_grad, [x1.shape[axis], x1.shape[axis] + x2.shape[axis]], axis)]`. """ axis = orig_call.attrs.axis assert axis is not None axis = int(axis) split_indices: list[Expr] = [] ty = orig_call.args[0].ty assert isinstance(ty, relax.TupleType) for i in range(len(ty.fields) - 1): tensor_ty = ty.fields[i] assert isinstance(tensor_ty, relax.TensorType) assert tensor_ty.shape is not None index = tensor_ty.shape[axis] if i > 0: index += split_indices[i - 1] split_indices.append(index) return [split(output_grad, split_indices, axis)] @register_gradient("relax.split") def split_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of split. Forward Form: `y = relax.split(x, indices, axis)` Backward: Returns `[concat(y_output_grad, axis)]`. """ axis = orig_call.attrs.axis axis = int(axis) return [concat(output_grad, axis)] @register_gradient("relax.expand_dims") def expand_dims_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of expand_dims. Forward Form: `y = relax.expand_dims(x, axis)` Backward: Returns `[squeeze_dims(y_grad, axis)]`. """ return [squeeze(output_grad, orig_call.attrs.axis)] @register_gradient("relax.reshape") def reshape_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of reshape. Forward Form: `y = relax.reshape(x, new_shape)` Backward: Returns `[reshape(y_grad, x.shape), no_grad]`. The second parameter, the target ShapeExpr, is not differentiable. """ return [ reshape(output_grad, _get_shape(orig_call.args[0])), no_grad(orig_call.args[1]), ] @register_gradient("relax.cumsum") def cumsum_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of cumsum. Forward Form: `y = relax.cumsum(x, axis)` Backward: The "reversed" cumsum along the same axis. Implemented by some tricks now. """ axis = orig_call.attrs.axis dtype = orig_call.attrs.dtype x_shape = _get_shape(orig_call.args[0]) if axis is not None: axis = int(axis) grad = sum(output_grad, axis, keepdims=True) - cumsum(output_grad, axis) + output_grad else: grad = reshape( sum(output_grad, keepdims=True) - cumsum(output_grad) + flatten(output_grad), x_shape ) if dtype is not None: grad = astype(grad, _get_dtype(orig_call.args[0])) return [grad] @register_gradient("relax.broadcast_to") def broadcast_to_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of broadcast_to. Forward Form: `y = relax.broadcast_to(x, new_shape)` Backward: Returns `[collapse_sum_to(y_grad, x.shape), no_grad]`. The second parameter, the target ShapeExpr, is not differentiable. """ return [ collapse_sum_to(output_grad, _get_shape(orig_call.args[0])), no_grad(orig_call.args[1]), ] ##################### Index ##################### @register_gradient("relax.take") def take_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of take. Forward Form: `y = relax.take(x, indices, axis)` Backward: Returns [x_grad, no_grad]. The second parameter, the indices, is not differentiable. """ axis = orig_call.attrs["axis"] return [ take_backward(output_grad, orig_call.args[0], orig_call.args[1], axis), no_grad(orig_call.args[1]), ] ##################### Search ##################### @register_gradient("relax.where") def where_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of where. Forward Form: `y = relax.where(cond, x1, x2)` Backward: Returns `[where(cond, y_grad, 0), where(cond, 0, y_grad)]`. The first parameter, the condition, is not differentiable. """ cond = orig_call.args[0] x1_zero = relax.const(0, _get_dtype(orig_call.args[1])) x2_zero = relax.const(0, _get_dtype(orig_call.args[2])) return [ no_grad(orig_call.args[0]), where(cond, output_grad, x1_zero), where(cond, x2_zero, output_grad), ] ##################### Linear Algebra ##################### @register_gradient("relax.matmul") def matmul_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of matmul. Forward Form: `c = relax.matmul(a, b)` Backward: Generally, returns `[c_grad @ b^T, a^T @ c_grad]`. Here we only transpose the last two dimensions because of the definition of batch matmul. Note that ndim=1 should be treaded specially. """ tensor_a, tensor_b = orig_call.args a_dim = len(_get_shape(tensor_a)) b_dim = len(_get_shape(tensor_b)) def _transpose_last_two_dim(tensor, ndim): """Helper function for reversing the last two dimensions.""" assert ndim > 1 return permute_dims( tensor, axes=[i if i < ndim - 2 else 2 * ndim - 3 - i for i in range(ndim)] ) if a_dim > 1 and b_dim > 1: a_grad = matmul(output_grad, _transpose_last_two_dim(tensor_b, b_dim)) b_grad = matmul(_transpose_last_two_dim(tensor_a, a_dim), output_grad) elif a_dim == 1 and b_dim > 1: a_expand = expand_dims(tensor_a, 1) grad_expand = expand_dims(output_grad, -2) a_grad = matmul(grad_expand, _transpose_last_two_dim(tensor_b, b_dim)) b_grad = matmul(a_expand, grad_expand) elif b_dim == 1 and a_dim > 1: b_expand = expand_dims(tensor_b, 0) grad_expand = expand_dims(output_grad, -1) a_grad = matmul(grad_expand, b_expand) b_grad = squeeze( matmul(_transpose_last_two_dim(tensor_a, a_dim), grad_expand), axis=-1 ) # squeeze last dim else: assert a_dim == 1 and b_dim == 1 a_grad = output_grad * tensor_b b_grad = output_grad * tensor_a return [ _fit_shape(ctx, a_grad, tensor_a), _fit_shape(ctx, b_grad, tensor_b), ] ##################### Datatype ##################### @register_gradient("relax.astype") def astype_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of astype. Forward Form: `y = relax.astype(x, dtype_of_y)` Backward: Returns `[astype(y_grad, dtype_of_x)]`. """ return [astype(output_grad, _get_dtype(orig_call.args[0]))] ##################### Neural network ##################### @register_gradient("relax.nn.relu") def relu_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of relu. Forward Form: `y = relax.relu(x)` Backward: Returns `[where(x < 0, 0, y_grad)]`. """ x = orig_call.args[0] zero = relax.const(0, _get_dtype(x)) return [where(less(x, zero), zero, output_grad)] @register_gradient("relax.nn.silu") def silu_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of silu. Forward Form: `y = relax.silu(x)` Backward: Returns `[y_grad * (sigmoid(x) + y * (1 - sigmoid(x)))]`. """ x = orig_call.args[0] sig = sigmoid(x) one = relax.const(1, _get_dtype(x)) return [output_grad * (sig + orig_var * (one - sig))] @register_gradient("relax.nn.softmax") def softmax_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of softmax. Forward Form: `y = relax.softmax(x, axis)` Backward: Returns `[(y_grad - sum(y_grad * y, axis, keepdims=True)) * y]` """ return [(output_grad - sum(output_grad * orig_var, orig_call.attrs.axis, True)) * orig_var] @register_gradient("relax.nn.log_softmax") def log_softmax_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of log_softmax. Forward Form: `y = relax.log_softmax(x, axis)` Backward: Returns `[y_grad - sum(y_grad, axis, keepdims=True) * exp(y)]` """ y_exp = exp(orig_var) return [output_grad - sum(output_grad, orig_call.attrs.axis, True) * y_exp] @register_gradient("relax.nn.cross_entropy_with_logits") def cross_entropy_with_logits_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of cross_entropy_with_logits. Forward Form: `z = relax.nn.cross_entropy_with_logits(x, y)` Backward: Returns `[-z_grad * y, -z_grad * x]`. If it has batch_size N, the results should divide by N. """ x, y = orig_call.args if x.ty.ndim > 1: batch_size = int(_get_shape(x)[0]) output_grad = output_grad / relax.const(batch_size, _get_dtype(output_grad)) return [-output_grad * y, -output_grad * x] # TODO(chaofan, yixin): remove nll_loss_backward and register the gradient using existing operators # This may require one_hot, strided_set, etc. @register_gradient("relax.nn.nll_loss") def nll_loss_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ): """Gradient of nll_loss. Forward Form: `z = relax.nn.nll_loss(predictions, targets, weights, reduction, ignore_index)` Suppose that `out = nll_loss(predictions, targets, weights, "none", ignore_index)`, and `z = reduction(out)` where reduction is in `["none", "mean", "sum"]`. Backward: First find the gradient w.r.t. `out`. Assume it is `out_grad`. Gererally, the gradient w.r.t. predictions is `predictions_grad[n, c, i_1, ..., i_k] = -o * w if c == t else 0`, where - `o = out_grad[n, i_1, ..., i_k]`, - `w = weights[n, i_1, ..., i_k]`, - `t = targets[n, i_1, ..., i_k]`. Additional checks are added if `ignore_index >= 0`, `weights=None`, or the predictions provided do not have batch. The gradient w.r.t. targets and weights are not available. """ pred_grad = nll_loss_backward( output_grad, orig_call.args[0], orig_call.args[1], weights=orig_call.args[2] if len(orig_call.args) == 3 else None, reduction=orig_call.attrs.reduction, ignore_index=orig_call.attrs.ignore_index, ) if len(orig_call.args) == 2: return [pred_grad, no_grad(orig_call.args[1])] return [pred_grad, no_grad(orig_call.args[1]), no_grad(orig_call.args[2])] @register_gradient("relax.nn.conv2d") def conv2d_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ) -> list[Expr]: """Gradient of conv2d. Now only supports `NCHW` data layout and `OIHW` kernel layout. Forward Form: `y = relax.nn.conv2d(x, weight, strides, padding, dilation, groups, data_layout, \ kernel_layout, out_layout, out_dtype)` Backward: Returns `[x_grad, weight_grad]` """ attrs = orig_call.attrs assert attrs.data_layout == "NCHW", "only support NCHW data layout" assert attrs.kernel_layout == "OIHW", "only support OIHW kernel layout" assert attrs.out_layout == "NCHW", "only support NCHW output layout" assert len(attrs.padding) == 4 assert len(attrs.strides) == 2 assert len(attrs.dilation) == 2 # calculate output_padding data, weight = orig_call.args batch, out_channel, grad_h, grad_w = _get_shape(orig_var) _, in_channel, in_h, in_w = _get_shape(data) _, _, filter_h, filter_w = _get_shape(weight) pad_top, pad_left, pad_bottom, pad_right = attrs.padding stride_h, stride_w = attrs.strides dilation_h, dilation_w = attrs.dilation out_h = (grad_h - 1) * stride_h - pad_top - pad_bottom + filter_h out_w = (grad_w - 1) * stride_w - pad_left - pad_right + filter_w output_padding = (int(in_h - out_h), int(in_w - out_w)) data_grad = conv2d_transpose( # type: ignore output_grad, orig_call.args[1], attrs.strides, attrs.padding, output_padding, attrs.dilation, attrs.groups, attrs.out_layout, attrs.kernel_layout[1] + attrs.kernel_layout[0] + attrs.kernel_layout[2:], attrs.data_layout, attrs.out_dtype, ) if attrs.groups != 1: data = reshape(data, (batch, attrs.groups, in_channel // attrs.groups, in_h, in_w)) data = permute_dims(data, [1, 0, 2, 3, 4]) data = reshape(data, (batch * attrs.groups, in_channel // attrs.groups, in_h, in_w)) weight_grad = conv2d( data, output_grad, strides=attrs.dilation, padding=attrs.padding, dilation=attrs.strides, groups=attrs.groups, out_dtype=attrs.out_dtype, data_layout="CNHW", kernel_layout="IOHW", out_layout="CNHW", ) # infer shape of weight_grad weight_grad_h = (in_h - (grad_h - 1) * stride_h - 1 + pad_top + pad_bottom) // dilation_h + 1 weight_grad_w = (in_w - (grad_w - 1) * stride_w - 1 + pad_left + pad_right) // dilation_w + 1 assert weight_grad_h >= filter_h assert weight_grad_w >= filter_w if weight_grad_h > filter_h or weight_grad_w > filter_w: weight_grad = strided_slice( weight_grad, axes=[0, 1, 2, 3], begin=[0, 0, 0, 0], end=[out_channel, in_channel // attrs.groups, filter_h, filter_w], ) return [data_grad, weight_grad] @register_gradient("relax.nn.max_pool2d") def max_pool2d_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ): """Gradient of max_pool2d. Forward Form: `y = relax.nn.max_pool2d(x, pool_size, strides, padding, dilation, ceil_mode, layout, \ out_layout)` Backward: Returns `[x_grad]` """ return [ max_pool2d_backward( # type: ignore output_grad, orig_call.args[0], orig_call.attrs.pool_size, orig_call.attrs.strides, orig_call.attrs.padding, orig_call.attrs.dilation, orig_call.attrs.ceil_mode, orig_call.attrs.count_include_pad, orig_call.attrs.layout, orig_call.attrs.out_layout, ) ] @register_gradient("relax.nn.avg_pool2d") def avg_pool2d_grad( orig_var: Var, orig_call: Call, output_grad: Var, ctx: BlockBuilder, ): """Gradient of avg_pool2d. Forward Form: `y = relax.nn.avg_pool2d(x, pool_size, strides, padding, dilation, ceil_mode, layout, \ out_layout)` Backward: Returns `[x_grad]` """ return [ avg_pool2d_backward( # type: ignore output_grad, orig_call.args[0], orig_call.attrs.pool_size, orig_call.attrs.strides, orig_call.attrs.padding, orig_call.attrs.dilation, orig_call.attrs.ceil_mode, orig_call.attrs.count_include_pad, orig_call.attrs.layout, orig_call.attrs.out_layout, ) ]