# 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.

from __future__ import print_function
import mxnet as mx
import numpy as np
import copy
from mxnet import autograd
from mxnet.gluon.model_zoo.vision import get_model
from mxnet.test_utils import assert_almost_equal
import sys
import os
import unittest
import pytest
curr_path = os.path.dirname(os.path.abspath(os.path.expanduser(__file__)))
sys.path.insert(0, os.path.join(curr_path, '../unittest'))

def eprint(*args, **kwargs):
    print(*args, file=sys.stderr, **kwargs)

VAL_DATA='data/val-5k-256.rec'
def download_data():
    return mx.test_utils.download(
        'https://repo.mxnet.io/gluon/dataset/test/val-5k-256-9e70d85e0.rec', VAL_DATA)

@mx.util.use_np
@pytest.mark.serial
# TODO(vcherepanov): mobilenet0.25 fails this test
@pytest.mark.parametrize('model_name', ['resnet50_v1', 'vgg19_bn', 'alexnet', 'densenet201', 'squeezenet1.0'])
def test_inference(model_name):
    batch_size = 10
    download_data()
    eprint(f'testing inference on {model_name}')

    data_shape = (3, 224, 224) if 'inception' not in model_name else (3, 299, 299)
    dataIter = mx.io.ImageRecordIter(
        path_imgrec        = VAL_DATA,
        label_width        = 1,
        preprocess_threads = 1,
        batch_size         = batch_size,
        data_shape         = data_shape,
        label_name         = 'softmax_label',
        rand_crop          = False,
        rand_mirror        = False)
    data_batch = dataIter.next()
    data = data_batch.data[0]
    label = data_batch.label[0]
    gpu_data = data.as_in_context(mx.gpu())
    gpu_label = label.as_in_context(mx.gpu())

    # This is to create a model and run the model once to initialize
    # all parameters.
    cpu_model = get_model(model_name)
    cpu_model.initialize(ctx=mx.cpu())
    cpu_model(mx.np.array(data, ctx=mx.cpu()))
    gpu_model = get_model(model_name)
    gpu_model.initialize(ctx=mx.gpu())
    gpu_model(mx.np.array(data, ctx=mx.gpu()))

    # Force the two models have the same parameters.
    cpu_params = cpu_model.collect_params()
    gpu_params = gpu_model.collect_params()
    for k in cpu_params.keys():
        cpu_param = cpu_params.get(k)
        gpu_param = gpu_params.get(k)
        gpu_param.set_data(cpu_param.data().as_in_context(mx.gpu()))

    cpu_data = mx.np.array(data, ctx=mx.cpu())
    for _ in range(5):
        # Run inference.
        with autograd.record(train_mode=False):
            cpu_out = cpu_model(cpu_data)
            gpu_out = gpu_model(gpu_data)

        max_val = np.max(np.abs(cpu_out.asnumpy()))
        gpu_max_val = np.max(np.abs(gpu_out.asnumpy()))
        eprint(model_name + ": CPU " + str(max_val) + ", GPU " + str(gpu_max_val))
        assert_almost_equal(cpu_out / max_val, gpu_out / gpu_max_val)

def get_nn_model(name):
    if "densenet" in name:
        return get_model(name, dropout=0)
    else:
        return get_model(name)

# Seed 1521019752 produced a failure on the Py2 oneDNN-GPU CI runner
# on 2/16/2018 that was not reproducible.  Problem could be timing related or
# based on non-deterministic algo selection.
@mx.util.use_np
@pytest.mark.serial
def test_training():
    # We use network models without dropout for testing.
    # TODO(zhengda) mobilenet can't pass this test even without oneDNN.
    all_models = ['resnet18_v1', 'densenet121']

    batch_size = 10
    label = mx.np.random.uniform(low=0, high=10, size=(batch_size)).astype('int32')

    download_data()
    dataIter = mx.io.ImageRecordIter(
        path_imgrec        = VAL_DATA,
        label_width        = 1,
        preprocess_threads = 1,
        batch_size         = batch_size,
        data_shape         = (3, 224, 224),
        label_name         = 'softmax_label',
        rand_crop          = False,
        rand_mirror        = False)
    data_batch = dataIter.next()
    data = data_batch.data[0]
    label = data_batch.label[0]
    gpu_data = data.as_in_context(mx.gpu())
    gpu_label = label.as_in_context(mx.gpu())
    softmax_cross_entropy = mx.gluon.loss.SoftmaxCrossEntropyLoss()

    for model_name in all_models:
        eprint(f'testing {model_name}')
        #data = mx.nd.random.uniform(shape=(100, 3, 224, 224))

        # This is to create a model and run the model once to initialize
        # all parameters.
        cpu_model = get_nn_model(model_name)
        cpu_model.initialize(ctx=mx.cpu())
        cpu_model(mx.np.array(data, ctx=mx.cpu()))
        gpu_model = get_nn_model(model_name)
        gpu_model.initialize(ctx=mx.gpu())
        gpu_model(mx.np.array(data, ctx=mx.gpu()))

        # Force the two models have the same parameters.
        cpu_params = cpu_model.collect_params()
        gpu_params = gpu_model.collect_params()
        for k in cpu_params.keys():
            cpu_param = cpu_params.get(k)
            gpu_param = gpu_params.get(k)
            gpu_param.set_data(cpu_param.data().as_in_context(mx.gpu()))

        cpu_trainer = mx.gluon.Trainer(cpu_params, 'sgd', {'learning_rate': 0.1})
        gpu_trainer = mx.gluon.Trainer(gpu_params, 'sgd', {'learning_rate': 0.1})

        # Run forward and backward once.
        with autograd.record():
            cpu_out = cpu_model(mx.np.array(data, ctx=mx.cpu()))
            gpu_out = gpu_model(gpu_data)
            cpu_loss = softmax_cross_entropy(cpu_out, label)
            gpu_loss = softmax_cross_entropy(gpu_out, gpu_label)
        max_val = np.max(np.abs(cpu_out.asnumpy()))
        gpu_max_val = np.max(np.abs(gpu_out.asnumpy()))
        eprint(model_name + ": CPU " + str(max_val) + ", GPU " + str(gpu_max_val))
        assert_almost_equal(cpu_out / max_val, gpu_out / max_val, rtol=1e-3, atol=1e-3)
        cpu_loss.backward()
        gpu_loss.backward()
        cpu_trainer.step(batch_size)
        gpu_trainer.step(batch_size)

        # Compare the parameters of the two models.
        start_test = False
        for k in cpu_params.keys():
            print(k)
            if "stage3" in k:
                start_test = True
            if (start_test):
                k = k.replace(cpu_params.prefix, '')
                cpu_param = cpu_params.get(k)
                gpu_param = gpu_params.get(k)
                assert_almost_equal(cpu_param.data(), gpu_param.data(), rtol=1e-3, atol=1e-3)

