# Copyright (c) 2016-present, Facebook, Inc.
#
# 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.
##############################################################################

## @package data_parallel_model_utils
# Module caffe2.python.data_parallel_model_utils
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

from future.utils import viewitems, viewkeys, viewvalues

import logging

from caffe2.python import core
from caffe2.python.data_parallel_model import stripBlobName

log = logging.getLogger("data_parallel_model_utils")
log.setLevel(logging.INFO)


def GetActivationBlobs(model):
    # Hacky way to get activations, think of a better way
    activations = []
    first_gpu_prefix = "{}_{}/".format(model._device_prefix, model._devices[0])

    all_inputs = set()
    for op in model.net.Proto().op:
        for inp in op.input:
            all_inputs.add(inp)

    params = set(model.GetParams(''))

    for op in model.net.Proto().op:
        for b in op.output:
            if b.startswith(first_gpu_prefix) and not b.endswith("_grad"):
                if b in all_inputs and b not in params and b + "_grad" in all_inputs:
                    activations.append(stripBlobName(b))
    return activations


def _ShiftActivationDevices(model, activations, from_device, to_device):
    prefix = "{}_{}/".format(model._device_prefix, from_device)
    activations = set([prefix + a for a in activations])
    all_activations = set([prefix + a for a in GetActivationBlobs(model)])
    ops = list(op for op in model.net.Proto().op if
               op.device_option.cuda_gpu_id == from_device)
    device_mapping = {a: to_device for a in activations}
    device_mapping.update({b: from_device for b in all_activations if
                           b not in activations})

    # Assign each blob to a device in a label propagation manner. activations
    # override, and if multiple activations in same op, the output activations
    # determine.
    for op in ops:
        op_device = None
        for b in list(op.input) + list(op.output):
            if b in device_mapping:
                if b in all_activations or op_device is None:
                    op_device = device_mapping[b]
        if op_device is None:
            op_device = op.device_option.cuda_gpu_id
        for b in list(op.input) + list(op.output):
            if b not in device_mapping and b.startswith(prefix):
                device_mapping[b] = op_device
        op.device_option.cuda_gpu_id = op_device

    # Change param_init_net accordingly
    for op in model.param_init_net.Proto().op:
        if op.output[0] in device_mapping:
            op.device_option.cuda_gpu_id = device_mapping[op.output[0]]


def ShiftActivationDevices(model, activations, shifts):
    '''
    Function to enable simple model-parallellism for data_parallel_model
    models. 'shifts' is a dictionary from_gpu -> to_gpu, and activations is
    a list of activation blobs (wout gpu_x/ prefix -- use GetActivationBlobs()).

    Operators handling these activations are shifted to the gpu declared in
    'shifts'. Also related operators such as gradient operators will be moved.
    Appropriate copy-ops are inserted.

    This allows shifting memory usage from one gpu to another, enabling bigger
    models to be trained.
    '''
    assert set(viewvalues(shifts)).intersection(set(viewkeys(shifts))) == set()
    for from_device, to_device in viewitems(shifts):
        log.info(
            "Shifting {} activations from {} --> {}".
            format(len(activations), from_device, to_device)
        )
        _ShiftActivationDevices(model, activations, from_device, to_device)

    param_init_net, blob_to_device = core.InjectCrossDeviceCopies(model.param_init_net)
    net, _blob_to_device = core.InjectCrossDeviceCopies(model.net, blob_to_device)
    model.param_init_net = param_init_net
    model.net = net
