|
- # -*- coding: utf-8 -*-
- # MegEngine is Licensed under the Apache License, Version 2.0 (the "License")
- #
- # Copyright (c) 2014-2021 Megvii Inc. All rights reserved.
- #
- # Unless required by applicable law or agreed to in writing,
- # software distributed under the License is distributed on an
- # "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- import inspect
- import io
- import itertools
- import random
- from tempfile import mkstemp
-
- import numpy as np
- import pytest
-
- import megengine.core.tensor.megbrain_graph as G
- import megengine.functional as F
- import megengine.optimizer as optim
- import megengine.utils.comp_graph_tools as cgtools
- from megengine import Parameter, tensor
- from megengine.autodiff import GradManager
- from megengine.core._trace_option import set_symbolic_shape
- from megengine.core.ops import builtin as ops
- from megengine.core.ops.builtin import Elemwise
- from megengine.core.tensor.utils import isscalar
- from megengine.functional import exp, log
- from megengine.jit import GraphOptimizationConfig, TraceError, exclude_from_trace, trace
- from megengine.module import Module
- from megengine.random import normal, uniform
- from megengine.utils.naming import AutoNaming
-
-
- @pytest.mark.parametrize("trace_mode", [False, True])
- @pytest.mark.parametrize("return_mode", ["Value", "Tuple", "List", "Dict"])
- def test_trace(trace_mode, return_mode):
- @trace(symbolic=trace_mode)
- def f(x):
- if return_mode == "Tuple":
- return (-x,)
- elif return_mode == "List":
- return [-x]
- elif return_mode == "Dict":
- return {"neg": -x}
- else:
- return -x
-
- def get_numpy(y):
- if return_mode == "Tuple" or return_mode == "List":
- return y[0].numpy()
- elif return_mode == "Dict":
- return y["neg"].numpy()
- return y.numpy()
-
- x = tensor([1])
- y = get_numpy(f(x))
-
- for i in range(3):
- np.testing.assert_equal(get_numpy(f(x)), y)
-
-
- def test_output_copy_trace():
- class Simple(Module):
- def __init__(self):
- super().__init__()
- self.a = Parameter([1.0], dtype=np.float32)
-
- def forward(self, x):
- x = x * self.a
- # will result into a copy of output in grad
- x = F.exp(x)
- return x
-
- ys = {False: [], True: []}
-
- for symbolic in [False, True]:
- net = Simple()
- gm = GradManager().attach(net.parameters())
- opt = optim.SGD(net.parameters(), 1e-3, momentum=0.9)
- data = tensor(np.arange(4).reshape(2, 2), dtype="float32")
-
- @trace(symbolic=symbolic)
- def train_func(d):
- with gm:
- loss = net(d)
- gm.backward(loss)
- opt.step().clear_grad()
- return loss
-
- for i in range(3):
- y = train_func(data).numpy()
- ys[symbolic].append(y)
-
- for i in range(3):
- np.testing.assert_equal(ys[False][i], ys[True][i])
-
-
- @pytest.mark.parametrize("trace_mode", [False, True])
- def test_tensor_detach(trace_mode):
- @trace(symbolic=True)
- def f(x):
- y = x.detach() ** 2
- z = y.detach() + 1
- return z.detach()
-
- x = tensor([1, 2, 3, 4])
- for _ in range(3):
- f(x).numpy()
-
-
- @pytest.mark.parametrize("trace_mode", [False, True])
- def test_exclude_from_trace(trace_mode):
- @trace(symbolic=trace_mode)
- def f(x):
- x = -x
- with exclude_from_trace():
- if i % 2:
- x = -x
- x = -x
- return x
-
- x = tensor([1])
-
- for i in range(3):
- y = f(x).numpy()
- np.testing.assert_equal(f(x).numpy(), y)
-
-
- @pytest.mark.parametrize("trace_mode", [False, True])
- def test_elemwise_fuse(trace_mode):
- # explicitly declare opt_level as 2
- @trace(symbolic=trace_mode, opt_level=2)
- def f(a, b):
- base = 0
- c = b - a
- _, idx = F.topk(c, 3)
- # internally, biased_idx will be idx as gopt will ignore the addition
- biased_idx = base + idx
- return biased_idx
-
- a = tensor(np.ones((7, 2)), dtype=np.int32)
- b = tensor(2 * np.ones((7, 2)), dtype=np.float32)
-
- for i in range(3):
- y = f(a, b)
- y.numpy()
-
-
- @pytest.mark.parametrize("trace_mode", [False, True])
- def test_elemwise_fuse_in_grad(trace_mode):
- w = Parameter(np.ones([4, 6]), dtype="float32")
-
- gm = GradManager().attach(w)
- opt = optim.SGD([w], lr=0.01, momentum=0.9, weight_decay=5e-4)
-
- # explicitly declare opt_level as 2
- @trace(symbolic=trace_mode, opt_level=2)
- def f():
- with gm:
- wm = F.sum(w ** 2, axis=1) ** 0.5
- loss = wm.mean()
- gm.backward(loss)
- opt.step().clear_grad()
- return loss
-
- for i in range(3):
- y = f()
- y.numpy()
-
-
- def test_print_in_trace():
- for symbolic in [False]: # cannot read value in symbolic mode
-
- @trace(symbolic=symbolic)
- def f(x):
- nonlocal buf
- x = -x
- buf = x.numpy()
- x = -x
- return x
-
- buf = None
- x = tensor([1])
-
- for i in range(3):
- y = f(x).numpy()
- z = buf
- buf = None
- np.testing.assert_equal(f(x).numpy(), y)
- np.testing.assert_equal(z, buf)
-
-
- @pytest.mark.parametrize(
- "dump_format",
- [
- "FBS",
- ],
- )
- def test_dump(dump_format):
- @trace(symbolic=True, capture_as_const=True)
- def f(a, b):
- return a + b
-
- # prevent from remaining scope from exception test
- AutoNaming.clear()
- a = tensor([2])
- b = tensor([4])
- y = f(a, b).numpy()
-
- for i in range(3):
- np.testing.assert_equal(f(a, b).numpy(), y)
-
- file = io.BytesIO()
- dump_info = f.dump(file, dump_format=dump_format)
- assert dump_info.nr_opr == 3
- np.testing.assert_equal(dump_info.inputs, ["arg_0", "arg_1"])
- np.testing.assert_equal(dump_info.outputs, ["ADD"])
- file.seek(0)
- infer_cg = cgtools.GraphInference(file)
- result = list((infer_cg.run(a, b)).values())[0]
- np.testing.assert_equal(result[0], y)
-
-
- def test_capture_dump():
- a = tensor([2])
-
- @trace(symbolic=True, capture_as_const=True)
- def f(x):
- return x * a
-
- x = tensor([3])
- y = f(x).numpy()
-
- for i in range(3):
- np.testing.assert_equal(f(x).numpy(), y)
-
- file = io.BytesIO()
- f.dump(file)
- file.seek(0)
- infer_cg = cgtools.GraphInference(file)
- result = list((infer_cg.run(x)).values())[0]
- np.testing.assert_equal(result[0], y)
-
-
- def test_dump_volatile():
- p = tensor([2])
-
- @trace(symbolic=True, capture_as_const=True)
- def f(x):
- return x * p
-
- x = tensor([3])
- y = f(x).numpy()
-
- for i in range(3):
- np.testing.assert_equal(f(x).numpy(), y)
-
- file = io.BytesIO()
- f.dump(file, optimize_for_inference=False)
- file.seek(0)
- (out,) = G.load_graph(file).output_vars_list
- assert (
- cgtools.get_owner_opr_type(cgtools.get_owner_opr_inputs(out)[1])
- == "ImmutableTensor"
- )
-
-
- def test_dump_backward_graph():
- x0 = tensor(np.random.randn(3, 4))
- x1 = tensor(np.random.randn(3, 4))
-
- gm = GradManager().attach(x0)
-
- @trace(symbolic=True, capture_as_const=True)
- def f(x0, x1):
- with gm:
- y = x0 * x1
- gm.backward(y, F.ones_like(y))
- dx0 = x0.grad
- return y, dx0
-
- y, dx0 = f(x0, x1)
- np.testing.assert_equal(dx0.numpy(), x1)
-
- file = io.BytesIO()
- f.dump(file, optimize_for_inference=False)
- file.seek(0)
-
- infer_cg = cgtools.GraphInference(file)
- results = list((infer_cg.run(x0, x1)).values())
-
- np.testing.assert_equal(results[0], y)
- np.testing.assert_equal(results[1], dx0)
-
-
- def test_dump_with_testcase():
- @trace(symbolic=True, capture_as_const=True)
- def f(x):
- return exp(x)
-
- f(tensor(1.0))
- file = io.BytesIO()
- f.dump(file, input_data=["#rand(0, 255, 1)"])
-
-
- @pytest.mark.parametrize("trace_mode", [False, True])
- def test_trace_profiler(trace_mode):
- @trace(symbolic=trace_mode, profiling=True)
- def f(x):
- return -x
-
- x = tensor([1])
- y = f(x).numpy()
-
- f(x)
- f(x) # XXX: has to run twice
-
- out = f.get_profile()
- assert out.get("profiler")
-
-
- def test_goptions():
- @trace(symbolic=True, opt_level=0, capture_as_const=True)
- def f(x):
- # directly return x / x will not trigger gopt
- # since there's no way to tell the two x are the same
- y = 2.0 * x
- return y / y
-
- @trace(symbolic=True, opt_level=1, capture_as_const=True)
- def g(x):
- y = 2.0 * x
- return y / y
-
- d = tensor(0.0)
- assert not np.isfinite(f(d).numpy())
- np.testing.assert_equal(g(d).numpy().item(), 1.0)
-
-
- def test_goptions_log_sum_exp():
- @trace(symbolic=True, opt_level=0, capture_as_const=True)
- def f(x, y):
- return log(exp(x) + exp(y))
-
- @trace(symbolic=True, opt_level=1, capture_as_const=True)
- def g(x, y):
- return log(exp(x) + exp(y))
-
- val = 1.0e4
- d = tensor(val)
- o = tensor(0.0)
- assert not np.isfinite(f(d, o).numpy())
- np.testing.assert_almost_equal(g(d, o), val)
-
-
- def test_goptions_log_exp():
- @trace(symbolic=True, opt_level=0, capture_as_const=True)
- def f(x):
- return log(exp(x))
-
- @trace(symbolic=True, opt_level=1, capture_as_const=True)
- def g(x):
- return log(exp(x))
-
- f(tensor(1.0))
- _, out = mkstemp()
- f.dump(out, optimize_for_inference=False)
- outputs = G.load_graph(out).output_vars_list
- oprs_1 = cgtools.get_oprs_seq(outputs)
-
- g(tensor(1.0))
- g.dump(out, optimize_for_inference=False)
- outputs = G.load_graph(out).output_vars_list
- oprs_2 = cgtools.get_oprs_seq(outputs)
-
- assert len(oprs_1) - len(oprs_2) == 2
-
-
- def test_optimize_for_inference():
- @trace(symbolic=True, capture_as_const=True)
- def f(x):
- return exp(x)
-
- _, out = mkstemp()
- f(tensor(5.0))
- f.dump(out, enable_io16xc32=True)
-
- res = G.load_graph(out)
- computing_input = res.output_vars_list[0].owner.inputs[0]
- assert computing_input.dtype == np.float16
-
-
- def test_optimize_for_inference_broadcast():
- a = tensor(np.ones(1, dtype=np.float32))
-
- @trace(capture_as_const=True, symbolic_shape=True)
- def f():
- return a._broadcast(tensor([1, 10], dtype=np.int32))
-
- f()
- f.dump(io.BytesIO())
-
-
- def test_trace_cvt_bool():
- x = tensor([0], dtype=np.int32)
-
- @trace(symbolic=True)
- def f(x):
- a = x.shape
- b = a[0]
- assert isscalar(b)
- return b == 0
-
- for i in range(3):
- np.testing.assert_equal(f(x).numpy(), False)
-
-
- @pytest.mark.parametrize("trace_mode", [False, True])
- def test_trace_reshape(trace_mode):
- x1 = tensor(np.random.randn(2, 10, 10))
- x2 = tensor(np.random.randn(4, 10, 10))
- x3 = tensor(np.random.randn(8, 10, 10))
-
- @trace(symbolic=trace_mode, capture_as_const=True)
- def f(x):
- y = x.reshape(x.shape[0], 100)
- return y
-
- f(x1)
- f(x2)
- f(x3)
-
-
- def test_trace_topk():
- x = tensor([5, 2, 7, 1, 0, 3, 2])
-
- @trace(symbolic=True)
- def f(x):
- y = F.topk(x, 3)
- np.testing.assert_equal(y[0].shape.numpy(), np.array([3,]))
- return y
-
- for i in range(3):
- f(x)
-
-
- def test_trace_warp_perspective():
- inp_shape = (1, 1, 4, 4)
- x = tensor(np.arange(16, dtype=np.float32).reshape(inp_shape))
- M_shape = (1, 3, 3)
- M = tensor(
- np.array(
- [[1.0, 0.0, 1.0], [0.0, 1.0, 1.0], [0.0, 0.0, 1.0]], dtype=np.float32
- ).reshape(M_shape)
- )
-
- @trace(symbolic=True)
- def f(x, M):
- out = F.vision.warp_perspective(x, M, (2, 2))
- np.testing.assert_equal(out.shape.numpy(), np.array([1, 1, 2, 2]))
- return out
-
- for i in range(3):
- f(x, M)
-
-
- @pytest.mark.parametrize(
- "normal_expr, mismatch_expr, reason",
- [
- ("a + b + c", "a + b - c", "operator mismatch"),
- ("a + b + 1", "a + b + 2", "tensors not equals"),
- ("((a + b), (b + c))[0]", "a + b", "mismature end"),
- ("a + b + c", "c + (a + b)", "expect internal node, got external"),
- ("c + (a + b)", "a + b + c", "expect external node, got internal"),
- ("a + b + c", "a + b + c + c", "too many instructions"),
- ("((a + b), (b + c))[1]", "((a + b), (b + c))[0]", "data unreadable"),
- ("((a + b), (b + c))[1] + a", "((a + b), (b + c))[0] + a", "input id mismatch"),
- ],
- )
- def test_trace_mismatch(normal_expr, mismatch_expr, reason):
- a = tensor([1, 2, 3, 4])
- b = tensor([5, 6, 7, 8])
- c = tensor([9, 0, 1, 2])
-
- mismatch = False
-
- @trace(symbolic=True)
- def fn(a, b, c):
- if not mismatch:
- result = eval(normal_expr)
- else:
- result = eval(mismatch_expr)
- return result
-
- for i in range(20):
- try:
- d = fn(a, b, c)
- except TraceError as e:
- assert mismatch
- assert str(e) == "trace error because {}".format(reason)
- except:
- pytest.fail("unexpected trace error")
- else:
- assert not mismatch
- np.testing.assert_equal(d.numpy(), eval(normal_expr).numpy())
- mismatch = random.random() > 0.8
-
-
- def test_exception_in_trace():
- a = tensor([1, 2, 3, 4])
- b = tensor([5, 6, 7, 8])
- c = tensor([9, 0, 1, 2])
-
- mismatch = False
-
- exc = Exception()
-
- @trace(symbolic=True)
- def fn(a, b, c):
- result = a + b
- if not mismatch:
- result += c
- else:
- raise exc
- return result
-
- for i in range(20):
- try:
- d = fn(a, b, c)
- except TraceError as e:
- pytest.fail("unexpected trace error")
- except Exception as e:
- assert mismatch
- assert e is exc
- else:
- assert not mismatch
- np.testing.assert_equal(d.numpy(), (a + b + c).numpy())
- mismatch = random.random() > 0.8
-
-
- def test_graph_error():
- a = tensor(np.arange(8).reshape((2, 4)))
- b = tensor(np.arange(8).reshape((2, 4)))
-
- @trace(symbolic=True)
- def fn(a, b):
- return a + b
-
- fn(a, b)
- with pytest.raises(RuntimeError):
- fn(a, b.transpose())
- fn(a, b)
-
-
- @pytest.mark.parametrize("trace_mode", [False, True])
- def test_trace_broadcast(trace_mode):
- x1 = tensor(np.random.randn(3, 1, 1))
- x2 = tensor(np.random.randn(1, 4, 1))
- x3 = tensor(np.random.randn(1, 1, 5))
-
- @trace(symbolic=trace_mode, capture_as_const=True)
- def f(x):
- y = F.broadcast_to(x, (3, 4, 5))
- return y
-
- f(x1)
- f(x2)
- f(x3)
-
-
- def test_trace_nms():
- def make_inputs(n):
- boxes = np.zeros((n, 4))
- boxes[:, :2] = np.random.rand(n, 2) * 100
- boxes[:, 2:] = np.random.rand(n, 2) * 100 + 100
-
- scores = np.random.rand(n)
-
- return tensor(boxes), tensor(scores)
-
- @trace(symbolic=False)
- def f(boxes, scores):
- # with tracing, max_output must be specified
- results = F.vision.nms(boxes, scores=scores, iou_thresh=0.5, max_output=20)
- # without tracing, max output can be inferred inside nms
- with exclude_from_trace():
- _ = F.vision.nms(boxes, scores=scores, iou_thresh=0.5)
- return results
-
- f(*make_inputs(10))
- f(*make_inputs(20))
- f(*make_inputs(30))
-
-
- def test_trace_valid_broadcast():
- x1 = tensor(np.random.randn(1, 1))
- x2 = tensor(np.random.randn(1, 2))
- shape = (tensor([2]), tensor([2]))
-
- @trace(symbolic=False)
- def f(x, shape):
- y = F.broadcast_to(x, shape)
- return y
-
- f(x1, shape)
- f(x2, shape)
-
-
- @pytest.mark.parametrize("trace_mode", [False, True])
- def test_clip(trace_mode):
- x = tensor(np.random.randn(10, 10))
-
- @trace(symbolic=trace_mode)
- def f(x, lower, upper):
- y = F.clip(x, lower, upper)
- return y
-
- for i in range(3):
- f(x, tensor([0]), tensor([1]))
-
- for i in range(3):
- f(x, tensor([5]), tensor([4]))
-
-
- # test returning noncontiguous tensor from trace
- def test_slice():
- @trace
- def f(x):
- return x[:, 1::2]
-
- x = F.arange(8).reshape(2, 4)
- f(x)
- y = f(x)
- np.testing.assert_array_equal(y.numpy(), x.numpy()[:, 1::2])
- y + y
-
-
- @pytest.mark.parametrize("shape_mode", [False, True])
- def test_random(shape_mode):
- def run_test(op):
- @trace(symbolic=True, symbolic_shape=shape_mode)
- def f():
- out = op(size=[10, 10])
- out_shape = out.shape
- assert out_shape is not None
- if not isinstance(out_shape, tuple):
- assert out.shape.numpy() is not None
- return out
-
- for _ in range(3):
- f()
-
- run_test(uniform)
- run_test(normal)
-
-
- @pytest.mark.parametrize("shape_mode", [False, True])
- def test_trace_advance_indexing(shape_mode):
- funcs = [
- lambda x, i: x[i],
- lambda x, i, j: x[i, j],
- lambda x, i, j: x[i, :, j, ...],
- lambda x, start, end: x[start:end],
- lambda x, start, end: x[:, 0, start:end, ..., 1],
- lambda x, vec: x[vec],
- lambda x, vec: x[vec, ..., 0, 1:3],
- lambda x, vec: x[vec, vec[0], vec[1]],
- # lambda x, i, start, end, vec: x[i, ..., :, vec, start:end], # FIXME
- lambda x, mask: x[mask],
- ]
-
- inputs = {
- "x": np.random.randn(5, 5, 5, 5, 5).astype("float32"),
- "i": 4,
- "j": 2,
- "start": 1,
- "end": 3,
- "vec": [1, 2, 3],
- "mask": np.random.randn(5, 5, 5, 5, 5) >= 0,
- }
- for f in funcs:
- sig = inspect.signature(f)
- param_names = list(sig._parameters.keys())
- params = {}
- params_np = {}
- f_traced = trace(f, symbolic=False, symbolic_shape=shape_mode)
- for name in param_names:
- params[name] = tensor(inputs[name])
- params_np[name] = inputs[name]
- expected = f(**params_np)
- result_imperative = f(**params)
- np.testing.assert_equal(expected, result_imperative.numpy())
- for _ in range(3):
- result_trace = f_traced(**params)
- np.testing.assert_equal(expected, result_trace.numpy())
-
-
- @pytest.mark.require_ngpu(1) # nvrtc backend
- def test_trace_jit_config():
- def run(fuse_dimshuffle, fuse_reduce):
- config = GraphOptimizationConfig()
- config.jit_fuse_dimshuffle = fuse_dimshuffle
- config.jit_fuse_reduce = fuse_reduce
-
- # set opt_level = 1 to avoid fusing dimshuffle and reduce at the same time
- @trace(opt_level=1, graph_opt_config=config)
- def func(x):
- return x + 1
-
- x = tensor(2)
- y = func(x)
- y = func(x)
- # func._compile()
-
- options = func._trace.options
- mapping = {None: 0, False: 1, True: 2}
- assert options.graph_opt.jit == 0
- assert options.graph_opt.jit_config.fuse_dimshuffle == mapping[fuse_dimshuffle]
- assert options.graph_opt.jit_config.fuse_reduce == mapping[fuse_reduce]
-
- for fuse_dimshuffle in [None, False, True]:
- for fuse_reduce in [None, False, True]:
- run(fuse_dimshuffle, fuse_reduce)
|