# 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. # ruff: noqa: E501, F811 """Unit tests for gradient with checkpointing.""" import tvm import tvm.testing from tvm import relax from tvm.ir.base import assert_structural_equal from tvm.relax.block_builder import BlockBuilder from tvm.relax.testing import nn from tvm.script.parser import ir as I from tvm.script.parser import relax as R def test_sequential(): """Comp. graph is a sequence""" # fmt: off @I.ir_module class Before: @R.function def main(x: R.Tensor((3, 3), "float32")): with R.dataflow(): x_scp = R.grad.start_checkpoint(x) lv1 = R.power(x_scp, R.const(3, "float32")) lv1_ecp = R.grad.end_checkpoint(lv1) lv2 = R.power(lv1_ecp, R.const(3, "float32")) lv2_scp = R.grad.start_checkpoint(lv2) lv3 = R.power(lv2_scp, R.const(3, "float32")) lv4 = R.power(lv3, R.const(3, "float32")) gv = R.sum(lv4) gv_ecp = R.grad.end_checkpoint(gv) R.output(gv_ecp) return gv_ecp @I.ir_module class Expected: @R.function def main_adjoint(x: R.Tensor((3, 3), "float32")) -> R.Tuple(R.Tensor((), "float32"), R.Tuple(R.Tensor((3, 3), "float32"))): with R.dataflow(): lv1: R.Tensor((3, 3), "float32") = R.power(x, R.const(3, "float32")) lv2: R.Tensor((3, 3), "float32") = R.power(lv1, R.const(3, "float32")) lv3: R.Tensor((3, 3), "float32") = R.power(lv2, R.const(3, "float32")) lv4: R.Tensor((3, 3), "float32") = R.power(lv3, R.const(3, "float32")) gv: R.Tensor((), "float32") = R.sum(lv4, axis=None, keepdims=False) gv_1: R.Tensor((), "float32") = gv gv_adjoint: R.Tensor((), "float32") = R.ones(R.shape([]), "float32") gv_adjoint1: R.Tensor((), "float32") = gv_adjoint lv3_cp: R.Tensor((3, 3), "float32") = R.power(lv2, R.const(3, "float32")) lv4_adjoint: R.Tensor((3, 3), "float32") = R.broadcast_to(gv_adjoint1, R.shape([3, 3])) lv: R.Tensor((3, 3), "float32") = R.multiply(lv4_adjoint, R.const(3, "float32")) lv1_1: R.Tensor((), "float32") = R.subtract(R.const(3, "float32"), R.const(1, "float32")) lv2_1: R.Tensor((3, 3), "float32") = R.power(lv3_cp, lv1_1) lv3_adjoint: R.Tensor((3, 3), "float32") = R.multiply(lv, lv2_1) lv6: R.Tensor((3, 3), "float32") = R.multiply(lv3_adjoint, R.const(3, "float32")) lv7: R.Tensor((), "float32") = R.subtract(R.const(3, "float32"), R.const(1, "float32")) lv8: R.Tensor((3, 3), "float32") = R.power(lv2, lv7) lv2_adjoint: R.Tensor((3, 3), "float32") = R.multiply(lv6, lv8) lv1_cp: R.Tensor((3, 3), "float32") = R.power(x, R.const(3, "float32")) lv12: R.Tensor((3, 3), "float32") = R.multiply(lv2_adjoint, R.const(3, "float32")) lv13: R.Tensor((), "float32") = R.subtract(R.const(3, "float32"), R.const(1, "float32")) lv14: R.Tensor((3, 3), "float32") = R.power(lv1_cp, lv13) lv1_adjoint: R.Tensor((3, 3), "float32") = R.multiply(lv12, lv14) lv18: R.Tensor((3, 3), "float32") = R.multiply(lv1_adjoint, R.const(3, "float32")) lv19: R.Tensor((), "float32") = R.subtract(R.const(3, "float32"), R.const(1, "float32")) lv20: R.Tensor((3, 3), "float32") = R.power(x, lv19) x_adjoint: R.Tensor((3, 3), "float32") = R.multiply(lv18, lv20) x_adjoint_out: R.Tensor((3, 3), "float32") = x_adjoint R.output(gv_1, x_adjoint_out) return (gv_1, (x_adjoint_out,)) @R.function def main(x: R.Tensor((3, 3), "float32")) -> R.Tensor((), "float32"): with R.dataflow(): x_scp: R.Tensor((3, 3), "float32") = R.grad.start_checkpoint(x) lv1: R.Tensor((3, 3), "float32") = R.power(x_scp, R.const(3, "float32")) lv1_ecp: R.Tensor((3, 3), "float32") = R.grad.end_checkpoint(lv1) lv2: R.Tensor((3, 3), "float32") = R.power(lv1_ecp, R.const(3, "float32")) lv2_scp: R.Tensor((3, 3), "float32") = R.grad.start_checkpoint(lv2) lv3: R.Tensor((3, 3), "float32") = R.power(lv2_scp, R.const(3, "float32")) lv4: R.Tensor((3, 3), "float32") = R.power(lv3, R.const(3, "float32")) gv: R.Tensor((), "float32") = R.sum(lv4, axis=None, keepdims=False) gv_ecp: R.Tensor((), "float32") = R.grad.end_checkpoint(gv) R.output(gv_ecp) return gv_ecp # fmt: on After = relax.transform.Gradient("main")(Before) assert_structural_equal(After, Expected) def test_sequential_consecutive(): """Comp. graph is a sequence""" # fmt: off @I.ir_module class Before: @R.function def main(x: R.Tensor((3, 3), "float32")): with R.dataflow(): x_scp = R.grad.start_checkpoint(x) lv1 = R.power(x_scp, R.const(3, "float32")) lv2 = R.power(lv1, R.const(3, "float32")) lv2_ecp = R.grad.end_checkpoint(lv2) lv2_scp = R.grad.start_checkpoint(lv2_ecp) lv3 = R.power(lv2_scp, R.const(3, "float32")) lv4 = R.power(lv3, R.const(3, "float32")) lv4_ecp = R.grad.end_checkpoint(lv4) gv = R.sum(lv4_ecp) R.output(gv) return gv @I.ir_module class Expected: @R.function def main_adjoint(x: R.Tensor((3, 3), "float32")) -> R.Tuple(R.Tensor((), "float32"), R.Tuple(R.Tensor((3, 3), "float32"))): with R.dataflow(): lv1: R.Tensor((3, 3), "float32") = R.power(x, R.const(3, "float32")) lv2: R.Tensor((3, 3), "float32") = R.power(lv1, R.const(3, "float32")) lv3: R.Tensor((3, 3), "float32") = R.power(lv2, R.const(3, "float32")) lv4: R.Tensor((3, 3), "float32") = R.power(lv3, R.const(3, "float32")) gv: R.Tensor((), "float32") = R.sum(lv4, axis=None, keepdims=False) gv_adjoint: R.Tensor((), "float32") = R.ones(R.shape([]), "float32") lv3_cp: R.Tensor((3, 3), "float32") = R.power(lv2, R.const(3, "float32")) lv4_adjoint: R.Tensor((3, 3), "float32") = R.broadcast_to(gv_adjoint, R.shape([3, 3])) lv: R.Tensor((3, 3), "float32") = R.multiply(lv4_adjoint, R.const(3, "float32")) lv1_1: R.Tensor((), "float32") = R.subtract(R.const(3, "float32"), R.const(1, "float32")) lv2_1: R.Tensor((3, 3), "float32") = R.power(lv3_cp, lv1_1) lv3_adjoint: R.Tensor((3, 3), "float32") = R.multiply(lv, lv2_1) lv6: R.Tensor((3, 3), "float32") = R.multiply(lv3_adjoint, R.const(3, "float32")) lv7: R.Tensor((), "float32") = R.subtract(R.const(3, "float32"), R.const(1, "float32")) lv8: R.Tensor((3, 3), "float32") = R.power(lv2, lv7) lv2_adjoint: R.Tensor((3, 3), "float32") = R.multiply(lv6, lv8) lv1_cp: R.Tensor((3, 3), "float32") = R.power(x, R.const(3, "float32")) lv12: R.Tensor((3, 3), "float32") = R.multiply(lv2_adjoint, R.const(3, "float32")) lv13: R.Tensor((), "float32") = R.subtract(R.const(3, "float32"), R.const(1, "float32")) lv14: R.Tensor((3, 3), "float32") = R.power(lv1_cp, lv13) lv1_adjoint: R.Tensor((3, 3), "float32") = R.multiply(lv12, lv14) lv18: R.Tensor((3, 3), "float32") = R.multiply(lv1_adjoint, R.const(3, "float32")) lv19: R.Tensor((), "float32") = R.subtract(R.const(3, "float32"), R.const(1, "float32")) lv20: R.Tensor((3, 3), "float32") = R.power(x, lv19) x_adjoint: R.Tensor((3, 3), "float32") = R.multiply(lv18, lv20) x_adjoint_out: R.Tensor((3, 3), "float32") = x_adjoint R.output(gv, x_adjoint_out) return (gv, (x_adjoint_out,)) @R.function def main(x: R.Tensor((3, 3), "float32")) -> R.Tensor((), "float32"): with R.dataflow(): x_scp: R.Tensor((3, 3), "float32") = R.grad.start_checkpoint(x) lv1: R.Tensor((3, 3), "float32") = R.power(x_scp, R.const(3, "float32")) lv2: R.Tensor((3, 3), "float32") = R.power(lv1, R.const(3, "float32")) lv2_ecp: R.Tensor((3, 3), "float32") = R.grad.end_checkpoint(lv2) lv2_scp: R.Tensor((3, 3), "float32") = R.grad.start_checkpoint(lv2_ecp) lv3: R.Tensor((3, 3), "float32") = R.power(lv2_scp, R.const(3, "float32")) lv4: R.Tensor((3, 3), "float32") = R.power(lv3, R.const(3, "float32")) lv4_ecp: R.Tensor((3, 3), "float32") = R.grad.end_checkpoint(lv4) gv: R.Tensor((), "float32") = R.sum(lv4_ecp, axis=None, keepdims=False) R.output(gv) return gv # fmt: on After = relax.transform.Gradient("main")(Before) assert_structural_equal(After, Expected) def test_tuple(): """Comp. graph is a sequence""" # fmt: off @I.ir_module class Before: @R.function def main(x: R.Tensor((3, 3), "float32")): with R.dataflow(): x_scp = R.grad.start_checkpoint(x) lv1 = R.power(x_scp, R.const(3, "float32")) lv2 = (x, lv1) lv3 = lv2 lv4 = R.power(lv3[0], R.const(3, "float32")) lv4_ecp = R.grad.end_checkpoint(lv4) gv = R.sum(lv4_ecp) R.output(gv) return gv @I.ir_module class Expected: @R.function def main_adjoint(x: R.Tensor((3, 3), "float32")) -> R.Tuple(R.Tensor((), "float32"), R.Tuple(R.Tensor((3, 3), "float32"))): with R.dataflow(): lv1: R.Tensor((3, 3), "float32") = R.power(x, R.const(3, "float32")) lv2: R.Tuple(R.Tensor((3, 3), "float32"), R.Tensor((3, 3), "float32")) = x, lv1 lv3: R.Tuple(R.Tensor((3, 3), "float32"), R.Tensor((3, 3), "float32")) = lv2 lv4: R.Tensor((3, 3), "float32") = lv3[0] lv4_1: R.Tensor((3, 3), "float32") = R.power(lv4, R.const(3, "float32")) gv: R.Tensor((), "float32") = R.sum(lv4_1, axis=None, keepdims=False) gv_adjoint: R.Tensor((), "float32") = R.ones(R.shape([]), "float32") lv1_cp: R.Tensor((3, 3), "float32") = R.power(x, R.const(3, "float32")) lv2_cp: R.Tuple(R.Tensor((3, 3), "float32"), R.Tensor((3, 3), "float32")) = x, lv1_cp lv3_cp: R.Tuple(R.Tensor((3, 3), "float32"), R.Tensor((3, 3), "float32")) = lv2_cp lv4_cp: R.Tensor((3, 3), "float32") = lv3_cp[0] lv4_adjoint: R.Tensor((3, 3), "float32") = R.broadcast_to(gv_adjoint, R.shape([3, 3])) lv: R.Tensor((3, 3), "float32") = R.multiply(lv4_adjoint, R.const(3, "float32")) lv1_1: R.Tensor((), "float32") = R.subtract(R.const(3, "float32"), R.const(1, "float32")) lv2_1: R.Tensor((3, 3), "float32") = R.power(lv4_cp, lv1_1) lv4_adjoint1: R.Tensor((3, 3), "float32") = R.multiply(lv, lv2_1) lv6: R.Tensor((3, 3), "float32") = R.zeros(R.shape([3, 3]), "float32") lv3_adjoint: R.Tuple(R.Tensor((3, 3), "float32"), R.Tensor((3, 3), "float32")) = lv4_adjoint1, lv6 lv2_adjoint: R.Tuple(R.Tensor((3, 3), "float32"), R.Tensor((3, 3), "float32")) = lv3_adjoint x_adjoint: R.Tensor((3, 3), "float32") = lv2_adjoint[0] lv1_adjoint: R.Tensor((3, 3), "float32") = lv2_adjoint[1] lv7: R.Tensor((3, 3), "float32") = R.multiply(lv1_adjoint, R.const(3, "float32")) lv8: R.Tensor((), "float32") = R.subtract(R.const(3, "float32"), R.const(1, "float32")) lv9: R.Tensor((3, 3), "float32") = R.power(x, lv8) lv12: R.Tensor((3, 3), "float32") = R.multiply(lv7, lv9) x_adjoint1: R.Tensor((3, 3), "float32") = R.add(x_adjoint, lv12) x_adjoint_out: R.Tensor((3, 3), "float32") = x_adjoint1 R.output(gv, x_adjoint_out) return (gv, (x_adjoint_out,)) @R.function def main(x: R.Tensor((3, 3), "float32")) -> R.Tensor((), "float32"): with R.dataflow(): x_scp: R.Tensor((3, 3), "float32") = R.grad.start_checkpoint(x) lv1: R.Tensor((3, 3), "float32") = R.power(x_scp, R.const(3, "float32")) lv2: R.Tuple(R.Tensor((3, 3), "float32"), R.Tensor((3, 3), "float32")) = x, lv1 lv3: R.Tuple(R.Tensor((3, 3), "float32"), R.Tensor((3, 3), "float32")) = lv2 lv4: R.Tensor((3, 3), "float32") = lv3[0] lv4_1: R.Tensor((3, 3), "float32") = R.power(lv4, R.const(3, "float32")) lv4_ecp: R.Tensor((3, 3), "float32") = R.grad.end_checkpoint(lv4_1) gv: R.Tensor((), "float32") = R.sum(lv4_ecp, axis=None, keepdims=False) R.output(gv) return gv # fmt: on After = relax.transform.Gradient("main")(Before) assert_structural_equal(After, Expected) def test_tree(): """Comp. graph is a output-directed tree""" # fmt: off @I.ir_module class Before: @R.function def main(x: R.Tensor((3, 3), "float32"), y: R.Tensor((3, 3), "float32"), z: R.Tensor((3, 3), "float32"), u: R.Tensor((3, 3), "float32"), v: R.Tensor((3, 3), "float32")): with R.dataflow(): lv1 = x * y lv1_scp = R.grad.start_checkpoint(lv1) z_scp = R.grad.start_checkpoint(z) lv2 = lv1_scp * z_scp lv2_ecp = R.grad.end_checkpoint(lv2) u_scp = R.grad.start_checkpoint(u) v_scp = R.grad.start_checkpoint(v) lv3 = u_scp * v_scp lv3_ecp = R.grad.end_checkpoint(lv3) lv4 = lv2_ecp * lv3_ecp gv = R.sum(lv4) R.output(gv) return gv @I.ir_module class Expected1: @R.function def main_adjoint(x: R.Tensor((3, 3), "float32"), y: R.Tensor((3, 3), "float32"), z: R.Tensor((3, 3), "float32"), u: R.Tensor((3, 3), "float32"), v: R.Tensor((3, 3), "float32")) -> R.Tuple(R.Tensor((), "float32"), R.Tuple(R.Tensor((3, 3), "float32"), R.Tensor((3, 3), "float32"), R.Tensor((3, 3), "float32"), R.Tensor((3, 3), "float32"), R.Tensor((3, 3), "float32"))): with R.dataflow(): lv1: R.Tensor((3, 3), "float32") = R.multiply(x, y) lv2: R.Tensor((3, 3), "float32") = R.multiply(lv1, z) lv3: R.Tensor((3, 3), "float32") = R.multiply(u, v) lv4: R.Tensor((3, 3), "float32") = R.multiply(lv2, lv3) gv: R.Tensor((), "float32") = R.sum(lv4, axis=None, keepdims=False) gv_adjoint: R.Tensor((), "float32") = R.ones(R.shape([]), "float32") lv4_adjoint: R.Tensor((3, 3), "float32") = R.broadcast_to(gv_adjoint, R.shape([3, 3])) lv2_cp: R.Tensor((3, 3), "float32") = R.multiply(lv1, z) lv3_cp: R.Tensor((3, 3), "float32") = R.multiply(u, v) lv2_adjoint: R.Tensor((3, 3), "float32") = R.multiply(lv4_adjoint, lv3_cp) lv3_adjoint: R.Tensor((3, 3), "float32") = R.multiply(lv4_adjoint, lv2_cp) u_adjoint: R.Tensor((3, 3), "float32") = R.multiply(lv3_adjoint, v) v_adjoint: R.Tensor((3, 3), "float32") = R.multiply(lv3_adjoint, u) lv1_adjoint: R.Tensor((3, 3), "float32") = R.multiply(lv2_adjoint, z) z_adjoint: R.Tensor((3, 3), "float32") = R.multiply(lv2_adjoint, lv1) x_adjoint: R.Tensor((3, 3), "float32") = R.multiply(lv1_adjoint, y) y_adjoint: R.Tensor((3, 3), "float32") = R.multiply(lv1_adjoint, x) x_adjoint_out: R.Tensor((3, 3), "float32") = x_adjoint y_adjoint_out: R.Tensor((3, 3), "float32") = y_adjoint z_adjoint_out: R.Tensor((3, 3), "float32") = z_adjoint u_adjoint_out: R.Tensor((3, 3), "float32") = u_adjoint v_adjoint_out: R.Tensor((3, 3), "float32") = v_adjoint R.output(gv, x_adjoint_out, y_adjoint_out, z_adjoint_out, u_adjoint_out, v_adjoint_out) return (gv, (x_adjoint_out, y_adjoint_out, z_adjoint_out, u_adjoint_out, v_adjoint_out)) @R.function def main(x: R.Tensor((3, 3), "float32"), y: R.Tensor((3, 3), "float32"), z: R.Tensor((3, 3), "float32"), u: R.Tensor((3, 3), "float32"), v: R.Tensor((3, 3), "float32")) -> R.Tensor((), "float32"): with R.dataflow(): lv1 = x * y lv1_scp = R.grad.start_checkpoint(lv1) z_scp = R.grad.start_checkpoint(z) lv2 = lv1_scp * z_scp lv2_ecp = R.grad.end_checkpoint(lv2) u_scp = R.grad.start_checkpoint(u) v_scp = R.grad.start_checkpoint(v) lv3 = u_scp * v_scp lv3_ecp = R.grad.end_checkpoint(lv3) lv4 = lv2_ecp * lv3_ecp gv = R.sum(lv4) R.output(gv) return gv # fmt: on After1 = relax.transform.Gradient("main")(Before) assert_structural_equal(After1, Expected1) # fmt: off @I.ir_module class Expected2: @R.function def main_adjoint(x: R.Tensor((3, 3), "float32"), y: R.Tensor((3, 3), "float32"), z: R.Tensor((3, 3), "float32"), u: R.Tensor((3, 3), "float32"), v: R.Tensor((3, 3), "float32")) -> R.Tuple(R.Tensor((), "float32"), R.Tuple(R.Tensor((3, 3), "float32"))): with R.dataflow(): lv1: R.Tensor((3, 3), "float32") = R.multiply(x, y) lv2: R.Tensor((3, 3), "float32") = R.multiply(lv1, z) lv3: R.Tensor((3, 3), "float32") = R.multiply(u, v) lv4: R.Tensor((3, 3), "float32") = R.multiply(lv2, lv3) gv: R.Tensor((), "float32") = R.sum(lv4, axis=None, keepdims=False) gv_adjoint: R.Tensor((), "float32") = R.ones(R.shape([]), "float32") lv4_adjoint: R.Tensor((3, 3), "float32") = R.broadcast_to(gv_adjoint, R.shape([3, 3])) lv3_cp: R.Tensor((3, 3), "float32") = R.multiply(u, v) lv2_adjoint: R.Tensor((3, 3), "float32") = R.multiply(lv4_adjoint, lv3_cp) z_adjoint: R.Tensor((3, 3), "float32") = R.multiply(lv2_adjoint, lv1) z_adjoint_out: R.Tensor((3, 3), "float32") = z_adjoint R.output(gv, z_adjoint_out) return (gv, (z_adjoint_out,)) @R.function def main(x: R.Tensor((3, 3), "float32"), y: R.Tensor((3, 3), "float32"), z: R.Tensor((3, 3), "float32"), u: R.Tensor((3, 3), "float32"), v: R.Tensor((3, 3), "float32")) -> R.Tensor((), "float32"): with R.dataflow(): lv1 = x * y lv1_scp = R.grad.start_checkpoint(lv1) z_scp = R.grad.start_checkpoint(z) lv2 = lv1_scp * z_scp lv2_ecp = R.grad.end_checkpoint(lv2) u_scp = R.grad.start_checkpoint(u) v_scp = R.grad.start_checkpoint(v) lv3 = u_scp * v_scp lv3_ecp = R.grad.end_checkpoint(lv3) lv4 = lv2_ecp * lv3_ecp gv = R.sum(lv4) R.output(gv) return gv # fmt: on After2 = relax.transform.Gradient("main", require_grads=Before["main"].params[2])(Before) assert_structural_equal(After2, Expected2) def test_dag(): """Comp. graph is a DAG with only one output. Here we only test the simple case: comp. graph is a sequence of sub-graphs, and the checkpoints are the intersections of connected subgraphs.""" # fmt: off @I.ir_module class Before: @R.function def main(x: R.Tensor((3, 3), "float32")): with R.dataflow(): lv = R.grad.start_checkpoint(x) lv1 = R.multiply(lv, R.const(2, "float32")) lv2 = R.multiply(lv1, R.const(2, "float32")) lv3 = R.grad.end_checkpoint(lv2) lv4 = R.multiply(x, lv3) lv5 = R.grad.start_checkpoint(lv4) lv6 = R.multiply(lv5, R.const(2, "float32")) lv7 = R.multiply(lv6, R.const(2, "float32")) lv8 = R.grad.end_checkpoint(lv7) lv9 = R.multiply(lv4, lv8) lv10 = R.grad.start_checkpoint(lv9) lv11 = R.multiply(lv10, R.const(2, "float32")) lv12 = R.multiply(lv11, R.const(2, "float32")) lv13 = R.grad.end_checkpoint(lv12) lv14 = R.multiply(lv9, lv13) gv: R.Tensor((), "float32") = R.sum(lv14, axis=None, keepdims=False) R.output(gv) return gv @I.ir_module class Expected: @R.function def main_adjoint(x: R.Tensor((3, 3), "float32")) -> R.Tuple(R.Tensor((), "float32"), R.Tuple(R.Tensor((3, 3), "float32"))): with R.dataflow(): lv1: R.Tensor((3, 3), "float32") = R.multiply(x, R.const(2, "float32")) lv2: R.Tensor((3, 3), "float32") = R.multiply(lv1, R.const(2, "float32")) lv3: R.Tensor((3, 3), "float32") = R.multiply(x, lv2) lv4: R.Tensor((3, 3), "float32") = R.multiply(lv3, R.const(2, "float32")) lv5: R.Tensor((3, 3), "float32") = R.multiply(lv4, R.const(2, "float32")) lv6: R.Tensor((3, 3), "float32") = R.multiply(lv3, lv5) lv7: R.Tensor((3, 3), "float32") = R.multiply(lv6, R.const(2, "float32")) lv8: R.Tensor((3, 3), "float32") = R.multiply(lv7, R.const(2, "float32")) lv9: R.Tensor((3, 3), "float32") = R.multiply(lv6, lv8) gv: R.Tensor((), "float32") = R.sum(lv9, axis=None, keepdims=False) gv_adjoint: R.Tensor((), "float32") = R.ones(R.shape([]), "float32") lv9_adjoint: R.Tensor((3, 3), "float32") = R.broadcast_to(gv_adjoint, R.shape([3, 3])) lv7_cp: R.Tensor((3, 3), "float32") = R.multiply(lv6, R.const(2, "float32")) lv8_cp: R.Tensor((3, 3), "float32") = R.multiply(lv7_cp, R.const(2, "float32")) lv6_adjoint: R.Tensor((3, 3), "float32") = R.multiply(lv9_adjoint, lv8_cp) lv8_adjoint: R.Tensor((3, 3), "float32") = R.multiply(lv9_adjoint, lv6) lv7_adjoint: R.Tensor((3, 3), "float32") = R.multiply(lv8_adjoint, R.const(2, "float32")) lv1_1: R.Tensor((3, 3), "float32") = R.multiply(lv7_adjoint, R.const(2, "float32")) lv6_adjoint1: R.Tensor((3, 3), "float32") = R.add(lv6_adjoint, lv1_1) lv4_cp: R.Tensor((3, 3), "float32") = R.multiply(lv3, R.const(2, "float32")) lv5_cp: R.Tensor((3, 3), "float32") = R.multiply(lv4_cp, R.const(2, "float32")) lv3_adjoint: R.Tensor((3, 3), "float32") = R.multiply(lv6_adjoint1, lv5_cp) lv5_adjoint: R.Tensor((3, 3), "float32") = R.multiply(lv6_adjoint1, lv3) lv4_adjoint: R.Tensor((3, 3), "float32") = R.multiply(lv5_adjoint, R.const(2, "float32")) lv4_1: R.Tensor((3, 3), "float32") = R.multiply(lv4_adjoint, R.const(2, "float32")) lv3_adjoint1: R.Tensor((3, 3), "float32") = R.add(lv3_adjoint, lv4_1) lv1_cp: R.Tensor((3, 3), "float32") = R.multiply(x, R.const(2, "float32")) lv2_cp: R.Tensor((3, 3), "float32") = R.multiply(lv1_cp, R.const(2, "float32")) x_adjoint: R.Tensor((3, 3), "float32") = R.multiply(lv3_adjoint1, lv2_cp) lv2_adjoint: R.Tensor((3, 3), "float32") = R.multiply(lv3_adjoint1, x) lv1_adjoint: R.Tensor((3, 3), "float32") = R.multiply(lv2_adjoint, R.const(2, "float32")) lv7_1: R.Tensor((3, 3), "float32") = R.multiply(lv1_adjoint, R.const(2, "float32")) x_adjoint1: R.Tensor((3, 3), "float32") = R.add(x_adjoint, lv7_1) x_adjoint_out: R.Tensor((3, 3), "float32") = x_adjoint1 R.output(gv, x_adjoint_out) return (gv, (x_adjoint_out,)) @R.function def main(x: R.Tensor((3, 3), "float32")) -> R.Tensor((), "float32"): with R.dataflow(): lv = R.grad.start_checkpoint(x) lv1 = R.multiply(lv, R.const(2, "float32")) lv2 = R.multiply(lv1, R.const(2, "float32")) lv3 = R.grad.end_checkpoint(lv2) lv4 = R.multiply(x, lv3) lv5 = R.grad.start_checkpoint(lv4) lv6 = R.multiply(lv5, R.const(2, "float32")) lv7 = R.multiply(lv6, R.const(2, "float32")) lv8 = R.grad.end_checkpoint(lv7) lv9 = R.multiply(lv4, lv8) lv10 = R.grad.start_checkpoint(lv9) lv11 = R.multiply(lv10, R.const(2, "float32")) lv12 = R.multiply(lv11, R.const(2, "float32")) lv13 = R.grad.end_checkpoint(lv12) lv14 = R.multiply(lv9, lv13) gv: R.Tensor((), "float32") = R.sum(lv14, axis=None, keepdims=False) R.output(gv) return gv # fmt: on After = relax.transform.Gradient("main")(Before) assert_structural_equal(After, Expected) def test_checkpoint_api(): """Test on tvm.relax.testing.nn.checkpoint API""" def func1(x): return relax.op.power(x, relax.const(3, "float32")) def func2(x): y = relax.op.power(relax.op.power(x, relax.const(3, "float32")), relax.const(3, "float32")) return relax.op.sum(y) bb = BlockBuilder() x = relax.Var("x", relax.TensorType((3, 3), "float32")) with bb.function("main", [x]): with bb.dataflow(): lv1 = bb.emit(nn.checkpoint(func1, x)) lv2 = bb.emit(relax.op.power(lv1, relax.const(3, "float32"))) lv3 = bb.emit_output(nn.checkpoint(func2, lv2)) bb.emit_func_output(lv3) # fmt: off @I.ir_module class Expected: @R.function def main(x: R.Tensor((3, 3), "float32")): with R.dataflow(): x_scp = R.grad.start_checkpoint(x) lv1 = R.power(x_scp, R.const(3, "float32")) lv1_ecp = R.grad.end_checkpoint(lv1) lv2 = R.power(lv1_ecp, R.const(3, "float32")) lv2_scp = R.grad.start_checkpoint(lv2) lv3 = R.power(lv2_scp, R.const(3, "float32")) lv4 = R.power(lv3, R.const(3, "float32")) gv = R.sum(lv4) gv_ecp = R.grad.end_checkpoint(gv) R.output(gv_ecp) return gv_ecp # fmt: on assert_structural_equal(bb.get(), Expected) def test_checkpoint_tree(): """Comp. graph is a output-directed tree""" def func(x, y, z, w): return x * y, z * w bb = BlockBuilder() x = relax.Var("x", relax.TensorType((3, 3), "float32")) y = relax.Var("y", relax.TensorType((3, 3), "float32")) z = relax.Var("z", relax.TensorType((3, 3), "float32")) u = relax.Var("u", relax.TensorType((3, 3), "float32")) v = relax.Var("v", relax.TensorType((3, 3), "float32")) with bb.function("main", [x, y, z, u, v]): with bb.dataflow(): lv1 = bb.emit(x * y) cp = nn.checkpoint(func, lv1, z, u, v) lv2 = bb.emit(cp[0]) lv3 = bb.emit(cp[1]) lv4 = bb.emit(lv2 * lv3) gv = bb.emit_output(relax.op.sum(lv4)) bb.emit_func_output(gv) # fmt: off @I.ir_module class Expected: @R.function def main(x: R.Tensor((3, 3), "float32"), y: R.Tensor((3, 3), "float32"), z: R.Tensor((3, 3), "float32"), u: R.Tensor((3, 3), "float32"), v: R.Tensor((3, 3), "float32")): with R.dataflow(): lv1 = x * y lv1_scp = R.grad.start_checkpoint(lv1) z_scp = R.grad.start_checkpoint(z) lv2 = lv1_scp * z_scp lv2_ecp = R.grad.end_checkpoint(lv2) u_scp = R.grad.start_checkpoint(u) v_scp = R.grad.start_checkpoint(v) lv3 = u_scp * v_scp lv3_ecp = R.grad.end_checkpoint(lv3) lv4 = lv2_ecp * lv3_ecp gv = R.sum(lv4) R.output(gv) return gv # fmt: on assert_structural_equal(bb.get(), Expected) def test_checkpoint_dag(): """Comp. graph is a DAG with only one output. Here we only test the simple case: comp. graph is a sequence of sub-graphs, and the checkpoints are the intersections of connected subgraphs.""" def func(x): return x * relax.const(2, "float32") * relax.const(2, "float32") bb = BlockBuilder() x = relax.Var("x", relax.TensorType((3, 3), "float32")) with bb.function("main", [x]): with bb.dataflow(): lv1 = bb.emit(nn.checkpoint(func, x)) lv2 = bb.emit(x * lv1) lv3 = bb.emit(nn.checkpoint(func, lv2)) lv4 = bb.emit(lv2 * lv3) lv5 = bb.emit(nn.checkpoint(func, lv4)) lv6 = bb.emit(lv4 * lv5) gv = bb.emit_output(relax.op.sum(lv6)) bb.emit_func_output(gv) # fmt: off @I.ir_module class Expected: @R.function def main(x: R.Tensor((3, 3), "float32")) -> R.Tensor((), "float32"): with R.dataflow(): lv = R.grad.start_checkpoint(x) lv1 = R.multiply(lv, R.const(2, "float32")) lv2 = R.multiply(lv1, R.const(2, "float32")) lv3 = R.grad.end_checkpoint(lv2) lv4 = R.multiply(x, lv3) lv5 = R.grad.start_checkpoint(lv4) lv6 = R.multiply(lv5, R.const(2, "float32")) lv7 = R.multiply(lv6, R.const(2, "float32")) lv8 = R.grad.end_checkpoint(lv7) lv9 = R.multiply(lv4, lv8) lv10 = R.grad.start_checkpoint(lv9) lv11 = R.multiply(lv10, R.const(2, "float32")) lv12 = R.multiply(lv11, R.const(2, "float32")) lv13 = R.grad.end_checkpoint(lv12) lv14 = R.multiply(lv9, lv13) gv: R.Tensor((), "float32") = R.sum(lv14, axis=None, keepdims=False) R.output(gv) return gv # fmt: on assert_structural_equal(bb.get(), Expected) def test_checkpoint_sequential(): def func(x): return x + x bb = BlockBuilder() x = relax.Var("x", relax.TensorType((3, 3), "float32")) with bb.function("main", [x]): with bb.dataflow(): lv1 = nn.emit_checkpoint_sequential([func] * 5, 2, x) lv2 = nn.emit_checkpoint_sequential([func] * 4, 2, lv1) gv = bb.emit_output(lv2) bb.emit_func_output(gv) # fmt: off @I.ir_module class Expected: @R.function def main(x: R.Tensor((3, 3), "float32")) -> R.Tensor((3, 3), "float32"): with R.dataflow(): x_scp: R.Tensor((3, 3), "float32") = R.grad.start_checkpoint(x) lv: R.Tensor((3, 3), "float32") = R.add(x_scp, x_scp) lv1: R.Tensor((3, 3), "float32") = R.add(lv, lv) lv1_ecp: R.Tensor((3, 3), "float32") = R.grad.end_checkpoint(lv1) lv1_ecp_scp: R.Tensor((3, 3), "float32") = R.grad.start_checkpoint(lv1_ecp) lv2: R.Tensor((3, 3), "float32") = R.add(lv1_ecp_scp, lv1_ecp_scp) lv3: R.Tensor((3, 3), "float32") = R.add(lv2, lv2) lv3_ecp: R.Tensor((3, 3), "float32") = R.grad.end_checkpoint(lv3) lv4: R.Tensor((3, 3), "float32") = R.add(lv3_ecp, lv3_ecp) lv4_scp: R.Tensor((3, 3), "float32") = R.grad.start_checkpoint(lv4) lv5: R.Tensor((3, 3), "float32") = R.add(lv4_scp, lv4_scp) lv6: R.Tensor((3, 3), "float32") = R.add(lv5, lv5) lv6_ecp: R.Tensor((3, 3), "float32") = R.grad.end_checkpoint(lv6) lv7: R.Tensor((3, 3), "float32") = R.add(lv6_ecp, lv6_ecp) lv8: R.Tensor((3, 3), "float32") = R.add(lv7, lv7) gv: R.Tensor((3, 3), "float32") = lv8 R.output(gv) return gv # fmt: on assert_structural_equal(bb.get(), Expected) def test_checkpoint_sequential_checkpoint_last(): def func(x): return x + x bb = BlockBuilder() x = relax.Var("x", relax.TensorType((3, 3), "float32")) with bb.function("main", [x]): with bb.dataflow(): lv1 = nn.emit_checkpoint_sequential([func] * 5, 2, x, checkpoint_last=True) lv2 = nn.emit_checkpoint_sequential([func] * 4, 2, lv1, checkpoint_last=True) gv = bb.emit_output(lv2) bb.emit_func_output(gv) # fmt: off @I.ir_module class Expected: @R.function def main(x: R.Tensor((3, 3), "float32")) -> R.Tensor((3, 3), "float32"): with R.dataflow(): x_scp: R.Tensor((3, 3), "float32") = R.grad.start_checkpoint(x) lv: R.Tensor((3, 3), "float32") = R.add(x_scp, x_scp) lv1: R.Tensor((3, 3), "float32") = R.add(lv, lv) lv1_ecp: R.Tensor((3, 3), "float32") = R.grad.end_checkpoint(lv1) lv1_ecp_scp: R.Tensor((3, 3), "float32") = R.grad.start_checkpoint(lv1_ecp) lv2: R.Tensor((3, 3), "float32") = R.add(lv1_ecp_scp, lv1_ecp_scp) lv3: R.Tensor((3, 3), "float32") = R.add(lv2, lv2) lv3_ecp: R.Tensor((3, 3), "float32") = R.grad.end_checkpoint(lv3) lv3_ecp_scp: R.Tensor((3, 3), "float32") = R.grad.start_checkpoint(lv3_ecp) lv4: R.Tensor((3, 3), "float32") = R.add(lv3_ecp_scp, lv3_ecp_scp) lv4_ecp: R.Tensor((3, 3), "float32") = R.grad.end_checkpoint(lv4) lv4_ecp_scp: R.Tensor((3, 3), "float32") = R.grad.start_checkpoint(lv4_ecp) lv5: R.Tensor((3, 3), "float32") = R.add(lv4_ecp_scp, lv4_ecp_scp) lv6: R.Tensor((3, 3), "float32") = R.add(lv5, lv5) lv6_ecp: R.Tensor((3, 3), "float32") = R.grad.end_checkpoint(lv6) lv6_ecp_scp: R.Tensor((3, 3), "float32") = R.grad.start_checkpoint(lv6_ecp) lv7: R.Tensor((3, 3), "float32") = R.add(lv6_ecp_scp, lv6_ecp_scp) lv8: R.Tensor((3, 3), "float32") = R.add(lv7, lv7) lv8_ecp: R.Tensor((3, 3), "float32") = R.grad.end_checkpoint(lv8) gv: R.Tensor((3, 3), "float32") = lv8_ecp R.output(gv) return gv # fmt: on assert_structural_equal(bb.get(), Expected) def test_checkpoint_dag(): """Comp. graph is a DAG with only one output. Here we only test the simple case: comp. graph is a sequence of sub-graphs, and the checkpoints are the intersections of connected subgraphs.""" def func(x): return x * relax.const(2, "float32") * relax.const(2, "float32") bb = BlockBuilder() x = relax.Var("x", relax.TensorType((3, 3), "float32")) with bb.function("main", [x]): with bb.dataflow(): lv1 = bb.emit(nn.checkpoint(func, x)) lv2 = bb.emit(x * lv1) lv3 = bb.emit(nn.checkpoint(func, lv2)) lv4 = bb.emit(lv2 * lv3) lv5 = bb.emit(nn.checkpoint(func, lv4)) lv6 = bb.emit(lv4 * lv5) gv = bb.emit_output(relax.op.sum(lv6)) bb.emit_func_output(gv) def test_checkpoint_with_intermediate_require_grads(): def func(x): return x * x * x bb = BlockBuilder() x = relax.Var("x", relax.TensorType((3, 3), "float32")) with bb.function("main", [x]): with bb.dataflow(): lv1 = nn.emit_checkpoint(func, x) gv = bb.emit_output(relax.op.sum(lv1)) bb.emit_func_output(gv) # fmt: off @I.ir_module class Expected: @R.function def main_adjoint(x: R.Tensor((3, 3), "float32")) -> R.Tuple(R.Tensor((), "float32"), R.Tuple(R.Tensor((3, 3), "float32"))): with R.dataflow(): lv: R.Tensor((3, 3), "float32") = R.multiply(x, x) lv1: R.Tensor((3, 3), "float32") = R.multiply(lv, x) gv: R.Tensor((), "float32") = R.sum(lv1, axis=None, keepdims=False) gv_adjoint: R.Tensor((), "float32") = R.ones(R.shape([]), "float32") lv1_adjoint: R.Tensor((3, 3), "float32") = R.broadcast_to(gv_adjoint, R.shape([3, 3])) lv1_adjoint_out: R.Tensor((3, 3), "float32") = lv1_adjoint R.output(gv, lv1_adjoint_out) return (gv, (lv1_adjoint_out,)) @R.function def main(x: R.Tensor((3, 3), "float32")) -> R.Tensor((), "float32"): with R.dataflow(): x_scp: R.Tensor((3, 3), "float32") = R.grad.start_checkpoint(x) lv: R.Tensor((3, 3), "float32") = R.multiply(x_scp, x_scp) lv1: R.Tensor((3, 3), "float32") = R.multiply(lv, x_scp) lv1_ecp: R.Tensor((3, 3), "float32") = R.grad.end_checkpoint(lv1) gv: R.Tensor((), "float32") = R.sum(lv1_ecp, axis=None, keepdims=False) R.output(gv) return gv # fmt: on After = relax.transform.Gradient("main", lv1)(bb.get()) assert_structural_equal(After, Expected) if __name__ == "__main__": tvm.testing.main()