# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed 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.

import os
import shutil
import unittest

from test_parallel_dygraph_dataparallel import TestMultipleGpus

import paddle
from paddle.distributed.fleet.utils.pp_parallel_adaptor import (
    ParallelConfig,
    PipeLineModelAdaptor,
    adaptor_from_args,
    parse_args,
)


class TestPPAdaptor(TestMultipleGpus):
    def test_parse_args(self):
        args = parse_args()
        self.assertEqual(args.src_mp, args.dst_mp)
        adaptor = adaptor_from_args(args)
        self.assertTrue(adaptor is not None)

    def test_hybrid_parallel_transformer_unbalanced_data(self):
        print(f"pwd {os.getcwd()}")
        self.run_mnist_2gpu('hybrid_parallel_pp_transformer_save.py')
        self.run_mnist_2gpu(
            'hybrid_parallel_pp_transformer_save_with_virtual_stage.py'
        )
        # test pp adaptor
        dir1 = "./pp_transformer"
        p_config1 = ParallelConfig(mp=1, pp=2, vpp=1, sharding=1)
        dir2 = "./pp_transformer_vp"
        p_config2 = ParallelConfig(mp=1, pp=2, vpp=2, sharding=1)

        pp_to_vp = PipeLineModelAdaptor(
            src_parallel_config=p_config1,
            dst_parallel_config=p_config2,
            transformer_layer_num=8,
            segment_method="layer",
        )
        vp_to_pp = PipeLineModelAdaptor(
            src_parallel_config=p_config2,
            dst_parallel_config=p_config1,
            transformer_layer_num=8,
            segment_method="layer",
        )

        def check_converted_model(converted_model_dir, expected_model_dir):
            # for compatibility, converted_model_dir may contain more key than
            # expected model, which does not hinder model recovering
            for i in range(p_config1.pp):
                sub_converted_model_dir = (
                    "{}/mp_00_sharding_00_pp_{:0>2d}".format(
                        converted_model_dir, i
                    )
                )
                sub_expected_model_dir = (
                    "{}/mp_00_sharding_00_pp_{:0>2d}".format(
                        expected_model_dir, i
                    )
                )
                print(
                    f"converted_model_dir: {sub_converted_model_dir}; expected_model_dir: {sub_expected_model_dir}"
                )

                def check_names(dict_1, dict_2):
                    for (k, v) in dict_2.items():
                        self.assertTrue(k in dict_1)
                        self.assertEqual(
                            getattr(v, "name", ""),
                            getattr(dict_1[k], "name", ""),
                        )

                # check param
                params_1 = paddle.load(
                    f"{sub_converted_model_dir}/model.pdparams"
                )
                params_2 = paddle.load(
                    f"{sub_expected_model_dir}/model.pdparams"
                )
                check_names(params_1, params_2)
                del params_1
                del params_2
                # check opt
                opt_1 = paddle.load(
                    f"{sub_converted_model_dir}/model_state.pdopt"
                )
                opt_2 = paddle.load(
                    f"{sub_expected_model_dir}/model_state.pdopt"
                )
                check_names(opt_1, opt_2)
                # check master wieghts
                if "master_weights" in opt_2:
                    self.assertTrue("master_weights" in opt_1)
                    check_names(
                        opt_2["master_weights"], opt_1["master_weights"]
                    )

        def create_dir_if_nonexist(dir: str):
            if not os.path.exists(dir):
                os.makedirs(dir)

        # check pp to vp
        tmp_dir1 = "./tmp_pp_to_vp"
        create_dir_if_nonexist(tmp_dir1)
        pp_to_vp.apply(dir1, tmp_dir1)
        # browse the converted model
        pp_to_vp.peek_model(tmp_dir1)
        # check
        check_converted_model(tmp_dir1, dir2)

        # check vp to pp
        tmp_dir2 = "./tmp_vp_to_pp"
        create_dir_if_nonexist(tmp_dir2)
        vp_to_pp.apply(dir2, tmp_dir2)
        vp_to_pp.peek_model(tmp_dir2)
        check_converted_model(tmp_dir2, dir1)

        # check uniform segment
        tmp_dir3 = "./tmp_vp_to_pp_uniform"
        create_dir_if_nonexist(tmp_dir3)
        vp_to_pp_uniform = PipeLineModelAdaptor(
            src_parallel_config=p_config2,
            dst_parallel_config=p_config1,
            transformer_layer_num=8,
            segment_method="uniform",
        )
        vp_to_pp_uniform.apply(dir2, tmp_dir3)
        vp_to_pp_uniform.peek_model(tmp_dir3)

        tmp_dir4 = "./tmp_pp_to_pp_uniform"
        create_dir_if_nonexist(tmp_dir4)
        pp_to_pp_uniform = PipeLineModelAdaptor(
            src_parallel_config=p_config1,
            dst_parallel_config=p_config1,
            transformer_layer_num=8,
            segment_method="uniform",
        )
        pp_to_pp_uniform.apply(dir1, tmp_dir4)
        pp_to_pp_uniform.peek_model(tmp_dir4)
        check_converted_model(tmp_dir3, tmp_dir4)

        # rm dirs
        for d in [dir1, dir2, tmp_dir1, tmp_dir2, tmp_dir3, tmp_dir4]:
            shutil.rmtree(d, ignore_errors=True)


if __name__ == "__main__":
    unittest.main()
