Files
wehub-resource-sync 26446540fa
Lint / lint (push) Waiting to run
CI / MacOS (push) Waiting to run
CI / Windows (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:36:25 +08:00

1318 lines
32 KiB
Python

# 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,
)
]