# Copyright (c) ModelScope Contributors. All rights reserved.
import torch.nn
import torch.nn.functional as F
from functools import partial

from swift.loss import loss_map
from swift.metrics import eval_metrics_map
from swift.utils import get_logger
from .base import BaseMegatronTrainer

logger = get_logger()


class MegatronEmbeddingTrainer(BaseMegatronTrainer):

    def __init__(self, args, template):
        super().__init__(args, template)
        self._loss_func = loss_map[args.loss_type](args, self)
        eval_metric = 'infonce' if args.loss_type == 'infonce' else 'paired'
        self.eval_metrics = eval_metrics_map[eval_metric](args, self)

    def loss_func(self,
                  output_tensor: torch.Tensor,
                  *,
                  labels: torch.Tensor,
                  packed_seq_params=None,
                  attention_mask=None):
        training = self.unwrapped_models[0].training
        last_hidden_state = self.get_last_tokens(output_tensor, packed_seq_params, attention_mask)
        if not training:
            self.eval_metrics.update(last_hidden_state.detach(), labels)
        mrl_dims = self.args.mrl_dims
        if mrl_dims:
            # Matryoshka Representation Learning: compute loss on each truncated dimension
            # and aggregate with the corresponding weights.
            loss = None
            for dim, weight in mrl_dims.items():
                if dim > last_hidden_state.shape[-1]:
                    logger.warning_once(f'MRL: skipping dimension {dim} because it exceeds the model hidden size '
                                        f'({last_hidden_state.shape[-1]}).')
                    continue
                sliced = F.normalize(last_hidden_state[..., :dim], p=2, dim=-1)
                cur_loss = weight * self._loss_func({'last_hidden_state': sliced}, labels)
                loss = cur_loss if loss is None else loss + cur_loss
        else:
            loss = self._loss_func({'last_hidden_state': last_hidden_state}, labels)
        metric = {'loss': loss.detach().clone()}
        metric = self._all_reduce_metric(metric)
        return loss, metric

    def forward_step(self, data_iterator, model):
        vp_stage = model.module.module.vp_stage
        data = self.get_batch(data_iterator, vp_stage)
        labels = data.pop('labels', None)
        output_tensor = model(**data)
        loss_func = partial(
            self.loss_func,
            labels=labels,
            packed_seq_params=data.get('packed_seq_params'),
            attention_mask=data.get('attention_mask')
            if data.get('attention_mask') is not None else data.get('attention_mask_2d'))
        return output_tensor, loss_func
