import copy
import torch
import torch.nn as nn
import torch.nn.functional as F
from dataclasses import dataclass
from typing import Dict, Optional
import math

try:
    from flash_attn import (
        flash_attn_varlen_qkvpacked_func,
        flash_attn_qkvpacked_func,
        flash_attn_varlen_kvpacked_func,
        flash_attn_kvpacked_func,
    )
    from flash_attn.bert_padding import unpad_input, pad_input
except ImportError:
    flash_attn_qkvpacked_func = None


@dataclass
class ModelArgs:
    @classmethod
    def from_config(cls, vocab, config):
        return cls(
            vocab_size=vocab.N,
            padding_idx=vocab.pad.index,
            n_layers=int(config["layers"]),
            n_heads=int(config["heads"]),
            dim=int(config["dim"]),
            enable_flash=bool(config["enable_flash"]),
            identifier=str(config["identifier"]),
            dropout=float(config["dropout"]),
            max_inference_seq_len=int(config["inference"]["max_decoder_seq_len"]),
            inference_batch_size=int(config["inference"]["batch_size"]),
            enable_cross_attention=bool(config["enable_cross_attention"]),
            max_batch_size=int(config["batch_size"]),
            max_seq_len=int(config["seq_len"]),
        )

    dim: int = 512
    n_layers: int = 8
    n_heads: int = 8
    vocab_size: int = -1  # defined later by tokenizer
    multiple_of: int = 256  # make SwiGLU hidden layer size multiple of large power of 2
    norm_eps: float = 1e-5
    dropout: float = 0.0
    cache: bool = True
    enable_flash: bool = True
    padding_idx: int = 0

    max_batch_size: int = 8
    inference_batch_size: int = 1
    max_seq_len: int = 512
    max_inference_seq_len: int = 512

    context_embedding_dim: int = 64
    enable_cross_attention: bool = False

    # TODO this should be automatically synced with the encoder's output dim
    cross_attention_embedding_dim: int = 1472

    identifier: str = "(unnamed)"


class RMSNorm(torch.nn.Module):
    def __init__(self, dim: int, eps: float = 1e-6):
        super().__init__()
        self.eps = eps
        self.weight = nn.Parameter(torch.ones(dim, dtype=torch.float32))

    def _norm(self, x):
        return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)

    def forward(self, x):
        output = self._norm(x.float()).type_as(x)
        return output * self.weight


class ReZero(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.alpha = nn.Parameter(torch.zeros(1, dtype=torch.float32))

    def forward(self, x):
        return x * self.alpha


def precompute_freqs_cis(dim: int, end: int, theta: float = 10000.0):
    freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)[: (dim // 2)].float() / dim))
    t = torch.arange(end, device=freqs.device)  # type: ignore
    freqs = torch.outer(t, freqs).float()  # type: ignore
    freqs_cis = torch.polar(torch.ones_like(freqs), freqs)  # complex64
    freqs_cis.requires_grad = False
    return freqs_cis


# freqs_cis = (seqlen, dim // 2) of complex64 unit phasors
# - - - - - - - -
# - - - - / / / /
# - - - / / / | |
# - - / / | | \ \
# - / | \ - / | \


# def reshape_for_broadcast(freqs_cis: torch.Tensor, x: torch.Tensor):
#     ndim = x.ndim
#     assert 0 <= 1 < ndim
#     assert freqs_cis.shape == (x.shape[1], x.shape[-1])
#     shape = [d if i == 1 or i == ndim - 1 else 1 for i, d in enumerate(x.shape)]
#     return freqs_cis.view(*shape)

# def apply_rotary_emb(
#     xq: torch.Tensor,
#     xk: torch.Tensor,
#     freqs_cis: torch.Tensor,
# ) -> Tuple[torch.Tensor, torch.Tensor]:
#     xq_ = torch.view_as_complex(xq.float().reshape(*xq.shape[:-1], -1, 2))
#     xk_ = torch.view_as_complex(xk.float().reshape(*xk.shape[:-1], -1, 2))
#     freqs_cis = reshape_for_broadcast(freqs_cis, xq_)
#     xq_out = torch.view_as_real(xq_ * freqs_cis).flatten(3)
#     xk_out = torch.view_as_real(xk_ * freqs_cis).flatten(3)
#     return xq_out.type_as(xq), xk_out.type_as(xk)


# jit-friendly version for one channel combining apply_rotary_emb and reshape_for_broadcast
def apply_rotary_emb(
    x: torch.Tensor,
    freqs_cis: torch.Tensor,
) -> torch.Tensor:
    bsz, seqlen, n_heads, _ = x.shape
    x_ = torch.view_as_complex(x.float().reshape(bsz, seqlen, n_heads, -1, 2))
    _, _, _, head_dim = x_.shape
    x_out = torch.view_as_real(x_ * freqs_cis.view(1, seqlen, 1, head_dim)).flatten(3)
    return x_out.type_as(x)


class Attention(nn.Module):
    def __init__(self, args: ModelArgs):
        super().__init__()

        self.n_heads = args.n_heads
        self.head_dim = args.dim // args.n_heads
        self.dropout_p = args.dropout

        self.wq = nn.Linear(
            args.dim,
            self.n_heads * self.head_dim,
            bias=False,
            dtype=torch.float32,
        )
        torch.nn.init.normal_(self.wq.weight, std=1 / math.sqrt(args.dim))
        self.wk = nn.Linear(
            args.dim,
            self.n_heads * self.head_dim,
            bias=False,
            dtype=torch.float32,
        )
        torch.nn.init.normal_(self.wk.weight, std=1 / math.sqrt(args.dim))
        self.wv = nn.Linear(
            args.dim,
            self.n_heads * self.head_dim,
            bias=False,
            dtype=torch.float32,
        )
        torch.nn.init.normal_(self.wv.weight, std=1 / math.sqrt(args.dim))
        self.wo = nn.Linear(
            self.n_heads * self.head_dim,
            args.dim,
            bias=False,
            dtype=torch.float32,
        )
        torch.nn.init.normal_(
            self.wo.weight, std=1 / math.sqrt(self.n_heads * self.head_dim)
        )

        self.enable_flash = args.enable_flash
        if self.enable_flash:
            assert flash_attn_qkvpacked_func is not None

        self.cache = args.cache
        if self.cache:
            self.cache_k = torch.zeros(
                (
                    args.max_batch_size,
                    args.max_seq_len,
                    self.n_heads,
                    self.head_dim,
                ),
                dtype=torch.float32,
                pin_memory=True if torch.cuda.is_available() else False,
                requires_grad=False,
            )
            self.cache_v = torch.zeros(
                (
                    args.max_batch_size,
                    args.max_seq_len,
                    self.n_heads,
                    self.head_dim,
                ),
                dtype=torch.float32,
                pin_memory=True if torch.cuda.is_available() else False,
                requires_grad=False,
            )

    @torch.jit.export
    def rearrange(self, idxs):
        if self.cache:
            self.cache_k[: idxs.shape[0]] = self.cache_k[idxs]
            self.cache_v[: idxs.shape[0]] = self.cache_v[idxs]

    @torch.jit.export
    def reallocate_caches(self):
        if self.cache:
            self.cache_k = torch.zeros_like(self.cache_k)
            self.cache_v = torch.zeros_like(self.cache_v)

    # to satisfy torch.jit
    @torch.jit.ignore
    def do_flash_attention(
        self,
        bsz: int,
        seqlen: int,
        mask: Optional[torch.Tensor],
        xq: torch.Tensor,
        keys: torch.Tensor,
        values: torch.Tensor,
    ) -> torch.Tensor:
        qkv = torch.stack((xq, keys, values), dim=2).to(torch.float16)
        if mask is not None:
            qkv, indices, cu_seqlens, max_seqlen = unpad_input(qkv, mask)
            output = flash_attn_varlen_qkvpacked_func(
                qkv,
                cu_seqlens,
                max_seqlen,
                causal=True,
                dropout_p=self.dropout_p if self.training else 0.0,
            )
            output = pad_input(output, indices, bsz, seqlen)
        else:
            output = flash_attn_qkvpacked_func(
                qkv, causal=True, dropout_p=self.dropout_p if self.training else 0.0
            )

        return output.to(torch.float32)

    def forward(
        self,
        x: torch.Tensor,
        start_pos: int,
        freqs_cis: torch.Tensor,
        mask: Optional[torch.Tensor],
    ):
        bsz, seqlen, _ = x.shape
        xq, xk, xv = self.wq(x), self.wk(x), self.wv(x)

        xq = xq.view(bsz, seqlen, self.n_heads, self.head_dim)
        xk = xk.view(bsz, seqlen, self.n_heads, self.head_dim)
        xv = xv.view(bsz, seqlen, self.n_heads, self.head_dim)

        xq = apply_rotary_emb(xq, freqs_cis=freqs_cis[start_pos : start_pos + seqlen])
        xk = apply_rotary_emb(xk, freqs_cis=freqs_cis[start_pos : start_pos + seqlen])

        if self.training or not self.cache:
            assert start_pos == 0
            keys = xk
            values = xv
        else:
            self.cache_k = self.cache_k.to(xq)
            self.cache_v = self.cache_v.to(xq)

            if bsz == 1:
                self.cache_k[:, start_pos : start_pos + seqlen] = xk
                self.cache_v[:, start_pos : start_pos + seqlen] = xv
            else:
                self.cache_k[:bsz, start_pos : start_pos + seqlen] = xk
                self.cache_v[:bsz, start_pos : start_pos + seqlen] = xv

            keys = self.cache_k[:bsz, : start_pos + seqlen]
            values = self.cache_v[:bsz, : start_pos + seqlen]

        if start_pos > 0 or not self.enable_flash:
            # naive implementation
            xq = xq.transpose(1, 2)
            keys = keys.transpose(1, 2)
            values = values.transpose(1, 2)
            scores = torch.matmul(xq, keys.transpose(2, 3)) / math.sqrt(self.head_dim)
            if mask is not None:
                scores = scores + mask  # (bs, n_local_heads, slen, cache_len + slen)
            scores = F.softmax(scores.float(), dim=-1).type_as(xq)
            output = torch.matmul(scores, values)  # (bs, n_local_heads, slen, head_dim)
            output = output.transpose(1, 2)
        else:
            # flash attention
            output = self.do_flash_attention(bsz, seqlen, mask, xq, keys, values)

        return self.wo(output.contiguous().view(bsz, seqlen, -1))


class CrossAttention(nn.Module):
    cached_xk: Optional[torch.Tensor]
    cached_xv: Optional[torch.Tensor]

    def __init__(self, args: ModelArgs):
        super().__init__()

        self.n_heads = args.n_heads
        self.head_dim = args.dim // args.n_heads
        self.embedding_dim = args.cross_attention_embedding_dim
        self.dropout_p = args.dropout

        self.wq = nn.Linear(
            args.dim,
            self.n_heads * self.head_dim,
            bias=False,
            dtype=torch.float32,
        )
        torch.nn.init.normal_(self.wq.weight, std=1 / math.sqrt(args.dim))
        self.wk = nn.Linear(
            self.embedding_dim,
            self.n_heads * self.head_dim,
            bias=False,
            dtype=torch.float32,
        )
        torch.nn.init.normal_(self.wk.weight, std=1 / math.sqrt(self.embedding_dim))
        self.wv = nn.Linear(
            self.embedding_dim,
            self.n_heads * self.head_dim,
            bias=False,
            dtype=torch.float32,
        )
        torch.nn.init.normal_(self.wv.weight, std=1 / math.sqrt(self.embedding_dim))
        self.wo = nn.Linear(
            self.n_heads * self.head_dim,
            args.dim,
            bias=False,
            dtype=torch.float32,
        )
        torch.nn.init.normal_(
            self.wo.weight, std=1 / math.sqrt(self.n_heads * self.head_dim)
        )

        self.cached_xk = None
        self.cached_xv = None

        self.enable_flash = args.enable_flash
        if self.enable_flash:
            assert flash_attn_qkvpacked_func is not None

    # to satisfy torch.jit
    @torch.jit.ignore
    def do_flash_attention(
        self,
        ebsz: int,
        bsz: int,
        seqlen: int,
        mask: Optional[torch.Tensor],
        xq: torch.Tensor,
        xk: torch.Tensor,
        xv: torch.Tensor,
    ) -> torch.Tensor:
        q = xq.to(torch.float16)
        kv = torch.stack((xk, xv), dim=2).to(torch.float16)
        if ebsz == 1:
            kv = kv.expand(bsz, -1, -1, -1, -1)
        if mask is not None:
            q = q.view(-1, self.n_heads, self.head_dim)
            cu_seqlens_q = torch.arange(
                0, (bsz + 1) * seqlen, seqlen, dtype=torch.int32, device=q.device
            )
            max_seqlen_q = seqlen
            if ebsz == 1:
                mask = mask.expand(bsz, -1)
            kv, indices, cu_seqlens_kv, max_seqlen_kv = unpad_input(kv, mask)
            output = flash_attn_varlen_kvpacked_func(
                q,
                kv,
                cu_seqlens_q,
                cu_seqlens_kv,
                max_seqlen_q,
                max_seqlen_kv,
                causal=False,
                dropout_p=self.dropout_p if self.training else 0.0,
            )
        else:
            output = flash_attn_kvpacked_func(
                q, kv, causal=False, dropout_p=self.dropout_p if self.training else 0.0
            )

        return output.to(torch.float32)

    def forward(
        self,
        x: torch.Tensor,
        encoder_out: Optional[torch.Tensor],
        start_pos: int,
        freqs_cis: torch.Tensor,
        mask: Optional[torch.Tensor],  # masks encoder_out
    ):
        bsz, seqlen, dim = x.shape

        if self.training:
            assert start_pos == 0

        if start_pos == 0:
            assert encoder_out is not None
            ebsz, encoder_seqlen, edim = encoder_out.shape
            assert edim == self.embedding_dim

            xk, xv = self.wk(encoder_out), self.wv(encoder_out)
            xk = xk.view(ebsz, encoder_seqlen, self.n_heads, self.head_dim)
            xv = xv.view(ebsz, encoder_seqlen, self.n_heads, self.head_dim)
            xk = apply_rotary_emb(xk, freqs_cis=freqs_cis[:encoder_seqlen])
            if not self.training:
                self.cached_xk = xk
                self.cached_xv = xv
        else:
            xk = self.cached_xk
            xv = self.cached_xv
            assert xk is not None and xv is not None
            ebsz, _, _, _ = xk.shape

        assert bsz == ebsz or ebsz == 1

        xq = self.wq(x)
        xq = xq.view(bsz, seqlen, self.n_heads, self.head_dim)
        xq = apply_rotary_emb(xq, freqs_cis=freqs_cis[start_pos : start_pos + seqlen])

        if start_pos > 0 or not self.enable_flash:
            # naive implementation
            xq = xq.transpose(1, 2)
            xk = xk.transpose(1, 2)
            xv = xv.transpose(1, 2)
            scores = torch.matmul(xq, xk.transpose(2, 3)) / math.sqrt(self.head_dim)
            if mask is not None:
                scores = scores + mask  # (bsz, n_heads, seqlen, encoder_seqlen)
            scores = F.softmax(scores.float(), dim=-1).type_as(xq)
            output = torch.matmul(scores, xv)  # (bsz, n_heads, seqlen, head_dim)
            output = output.transpose(1, 2)
        else:
            # flash attention
            output = self.do_flash_attention(ebsz, bsz, seqlen, mask, xq, xk, xv)

        return self.wo(output.contiguous().view(bsz, seqlen, -1))

    @torch.jit.export
    def reallocate_caches(self):
        self.cached_xk = None
        self.cached_xv = None

    @torch.jit.export
    def rearrange(self, idxs):
        # this weirdness is required to get Optional[T] type refinement working
        cached_xk = self.cached_xk
        cached_xv = self.cached_xv
        if cached_xk is not None and cached_xv is not None:
            self.cached_xk = cached_xk[idxs].contiguous()
            self.cached_xv = cached_xv[idxs].contiguous()


class FeedForward(nn.Module):
    def __init__(
        self,
        dim: int,
        hidden_dim: int,
        multiple_of: int,
        dropout_p: float,
    ):
        super().__init__()
        hidden_dim = int(2 * hidden_dim / 3)
        hidden_dim = multiple_of * ((hidden_dim + multiple_of - 1) // multiple_of)

        self.w1 = nn.Linear(dim, hidden_dim, bias=False, dtype=torch.float32)
        torch.nn.init.normal_(self.w1.weight, std=1 / math.sqrt(dim))
        self.w2 = nn.Linear(hidden_dim, dim, bias=False, dtype=torch.float32)
        torch.nn.init.normal_(self.w2.weight, std=1 / math.sqrt(hidden_dim))
        self.w3 = nn.Linear(dim, hidden_dim, bias=False, dtype=torch.float32)
        torch.nn.init.normal_(self.w3.weight, std=1 / math.sqrt(dim))
        if dropout_p > 0.0:
            self.dropout: nn.Module = nn.Dropout(dropout_p)
        else:
            self.dropout = nn.Identity()

    def forward(self, x):
        return self.dropout(self.w2(F.silu(self.w1(x)) * self.w3(x)))


class TransformerBlock(nn.Module):
    def __init__(self, layer_id: int, args: ModelArgs):
        super().__init__()
        self.n_heads = args.n_heads
        self.dim = args.dim
        self.head_dim = args.dim // args.n_heads
        self.attention = Attention(args)
        self.feed_forward = FeedForward(
            dim=args.dim,
            hidden_dim=4 * args.dim,
            multiple_of=args.multiple_of,
            dropout_p=args.dropout,
        )
        self.layer_id = layer_id
        self.attention_norm = RMSNorm(args.dim, eps=args.norm_eps)
        self.ffn_norm = RMSNorm(args.dim, eps=args.norm_eps)
        self.rezero = ReZero()
        self.enable_cross_attention = args.enable_cross_attention
        if args.enable_cross_attention:
            self.cross_attention = CrossAttention(args)
            self.cross_attention_norm = RMSNorm(args.dim, eps=args.norm_eps)

    def forward(
        self,
        x: torch.Tensor,
        start_pos: int,
        freqs_cis: torch.Tensor,
        mask: Optional[torch.Tensor],
        encoder_out: Optional[torch.Tensor],
        mask_encoder_out: Optional[torch.Tensor],
    ):
        h = x + self.rezero(
            self.attention.forward(self.attention_norm(x), start_pos, freqs_cis, mask)
        )
        if self.enable_cross_attention:
            h = h + self.rezero(
                self.cross_attention.forward(
                    self.cross_attention_norm(h),
                    encoder_out,
                    start_pos,
                    freqs_cis,
                    mask_encoder_out,
                )
            )
        out = h + self.rezero(self.feed_forward.forward(self.ffn_norm(h)))
        return out

    @torch.jit.export
    def rearrange(self, idxs):
        self.attention.rearrange(idxs)
        if self.enable_cross_attention:
            self.cross_attention.rearrange(idxs)

    @torch.jit.export
    def reallocate_caches(self):
        self.attention.reallocate_caches()
        if self.enable_cross_attention:
            self.cross_attention.reallocate_caches()


# for torch.jit.script
class TransformerBlockSequence(nn.Module):
    def __init__(self, module_list):
        super().__init__()
        self.layers = module_list

    def forward(
        self,
        x: torch.Tensor,
        start_pos: int,
        freqs_cis: torch.Tensor,
        mask: Optional[torch.Tensor],
        encoder_out: Optional[torch.Tensor],
        mask_encoder_out: Optional[torch.Tensor],
        second_last_layer: Optional[bool] = False,
    ):
        for layer in self.layers if not second_last_layer else self.layers[:-1]:
            x = layer(x, start_pos, freqs_cis, mask, encoder_out, mask_encoder_out)
        return x

    def jit(self):
        torch.jit.enable_onednn_fusion(False)
        for i, layer in enumerate(self.layers):
            self.layers[i] = torch.jit.script(layer)

    @torch.jit.export
    def rearrange(self, idxs):
        for layer in self.layers:
            layer.rearrange(idxs)

    @torch.jit.export
    def reallocate_caches(self):
        for layer in self.layers:
            layer.reallocate_caches()


class Transformer(nn.Module):
    def __init__(self, params: ModelArgs):
        super().__init__()
        self.params = params
        self.vocab_size = params.vocab_size
        self.n_layers = params.n_layers

        self.tok_embeddings = nn.Embedding(params.vocab_size, params.dim)
        torch.nn.init.normal_(self.tok_embeddings.weight)
        self.input_dropout = nn.Dropout(params.dropout)
        self.final_dropout = nn.Dropout(params.dropout)

        layers = torch.nn.ModuleList()
        for layer_id in range(params.n_layers):
            layers.append(TransformerBlock(layer_id, params))
        self.layer_sequence = TransformerBlockSequence(layers)

        self.norm = RMSNorm(params.dim, eps=params.norm_eps)
        self.output = nn.Linear(params.dim, params.vocab_size, bias=False)
        torch.nn.init.normal_(self.output.weight, std=1 / math.sqrt(params.dim))

        self.freqs_cis = precompute_freqs_cis(
            self.params.dim // self.params.n_heads, self.params.max_seq_len * 2
        )

        self.cache_pad_mask = None

    def jit(self):
        # This is slower and triggers https://github.com/pytorch/pytorch/issues/75903
        # self.layer_sequence = torch.jit.script(self.layer_sequence)
        self.layer_sequence.jit()

    def rearrange(self, idxs):
        self.layer_sequence.rearrange(idxs)
        if self.cache_pad_mask is not None:
            self.cache_pad_mask = self.cache_pad_mask[idxs]

    def reallocate_caches(self):
        self.layer_sequence.reallocate_caches()
        self.cache_pad_mask = None

    def forward(
        self,
        tokens: torch.Tensor,
        start_pos: int,
        aux_embeddings: Optional[torch.Tensor] = None,
        return_embeddings: bool = False,
        encoder_out: Optional[torch.Tensor] = None,
        encoder_out_mask: Optional[
            torch.Tensor
        ] = None,  # (bsz, encoder_seqlen) of bools (True = valid, False = pad)
    ):
        assert len(tokens.shape) == 2

        bsz, _ = tokens.shape
        h = self.tok_embeddings(tokens)
        self.freqs_cis = self.freqs_cis.to(h.device)

        if aux_embeddings is not None:
            h += aux_embeddings.to(h.device)

        extra_start_token = False
        if start_pos == 0:
            if self.params.enable_cross_attention:
                assert encoder_out is not None
            elif encoder_out is not None:
                # place encoder_out last non-pad embeddings as first embeddings for decoder
                if encoder_out_mask is not None:
                    last_embeddings = encoder_out[
                        torch.arange(encoder_out.shape[0], device=tokens.device),
                        (encoder_out_mask.sum(dim=1) - 1).long(),
                    ]
                else:
                    last_embeddings = encoder_out[:, -1]

                if last_embeddings.shape[0] == 1 and bsz > 1:
                    last_embeddings = last_embeddings.repeat(bsz, 1)
                h = torch.cat([last_embeddings.unsqueeze(1), h], dim=1)
                extra_start_token = True

        seqlen = h.shape[1]

        if (
            encoder_out is not None
            and not self.params.enable_cross_attention
            and start_pos > 0
        ):
            # tricky - move start_pos because position 0 was used for encoder output
            start_pos += 1

        key_padding_mask = tokens != self.params.padding_idx
        if extra_start_token:
            # shift mask
            key_padding_mask = torch.cat(
                [
                    torch.ones(
                        bsz, 1, device=tokens.device, dtype=key_padding_mask.dtype
                    ),
                    key_padding_mask,
                ],
                dim=1,
            )

        if start_pos == 0 and self.params.enable_flash:
            # flash attention
            # special case - flash attention crashes if all positions are invalid
            # force the first position to be valid (won't matter for loss since it's masked)
            if seqlen > 0:
                allpad_examples = torch.all(torch.logical_not(key_padding_mask), dim=1)
                key_padding_mask[allpad_examples, 0] = True

            self.cache_pad_mask = key_padding_mask
            mask = key_padding_mask
        else:
            # naive attention
            if self.cache_pad_mask is not None and self.cache_pad_mask.shape[0] < bsz:
                self.cache_pad_mask = self.cache_pad_mask.repeat(bsz, 1)
            self.cache_pad_mask = (
                torch.cat(
                    [self.cache_pad_mask[:bsz, :start_pos], key_padding_mask], dim=1
                )
                if start_pos > 0
                else key_padding_mask
            )
            mask = torch.full(
                (bsz, seqlen, start_pos + seqlen), float("-inf"), device=tokens.device
            )
            mask.triu_(diagonal=1 + start_pos)
            mask = mask.type_as(h)

            # only if some positions are not valid
            if not torch.all(self.cache_pad_mask):
                row_diagi = torch.arange(0, seqlen, device=tokens.device)
                col_diagi = torch.arange(
                    start_pos, start_pos + seqlen, device=tokens.device
                )
                mask.masked_fill_(
                    self.cache_pad_mask.unsqueeze(1) == False, float("-inf")
                )
                mask[:, row_diagi, col_diagi] = 0.0

            mask = mask.unsqueeze(1)

            if encoder_out_mask is not None and self.params.enable_cross_attention:
                if not torch.all(encoder_out_mask):
                    full_encoder_out_mask = torch.zeros(
                        bsz, seqlen, encoder_out.shape[1], device=tokens.device
                    )
                    full_encoder_out_mask.masked_fill_(
                        encoder_out_mask.unsqueeze(1) == False, float("-inf")
                    )
                    encoder_out_mask = full_encoder_out_mask.unsqueeze(1)
                else:
                    encoder_out_mask = None

        h = self.input_dropout(h)

        h = self.layer_sequence(
            h,
            start_pos,
            self.freqs_cis,
            mask,
            encoder_out,
            encoder_out_mask,
            second_last_layer=return_embeddings,
        )

        if return_embeddings:
            out = h
        else:
            h = self.final_dropout(h)
            h = self.norm(h)
            out = F.log_softmax(self.output(h), dim=-1)

        if extra_start_token:
            out = out[:, 1:]

        return out


class LabelSmoothing(nn.Module):
    "Implement label smoothing."

    def __init__(self, padding_idx, smoothing=0.0):
        super().__init__()
        self.criterion = nn.KLDivLoss(reduction="sum")
        self.padding_idx = padding_idx
        self.confidence = 1.0 - smoothing
        self.smoothing = smoothing
        self.true_dist = None

    def forward(self, x, target):
        true_dist = x.data.clone()
        true_dist.fill_(self.smoothing / (x.size(1) - 2))
        true_dist.scatter_(1, target.data.unsqueeze(1), self.confidence)
        true_dist[:, self.padding_idx] = 0
        mask = torch.nonzero(target.data == self.padding_idx)
        if mask.dim() > 0:
            true_dist.index_fill_(0, mask.squeeze(), 0.0)
        self.true_dist = true_dist
        return self.criterion(x, true_dist.clone().detach())


class SimpleLossCompute:
    "A simple loss compute and train function."

    def __init__(self, criterion):
        self.criterion = criterion

    def __call__(self, x, y):
        return self.criterion(
            x.contiguous().view(-1, x.size(-1)), y.contiguous().view(-1)
        )


class MusicalPositionEmbedTransformer(nn.Module):
    def __init__(self, vocab, params, encoder=None):
        super().__init__()
        self.encoder = encoder
        self.vocab = vocab
        self.params = params
        self.transformer = Transformer(params)
        beats_max = vocab.embed_length_max // vocab.quantize_divisions
        self.bar_embedding = nn.Embedding(
            beats_max // 4 + 1, params.context_embedding_dim
        )
        torch.nn.init.normal_(self.bar_embedding.weight)
        self.beat_embedding = nn.Embedding(4, params.context_embedding_dim)
        torch.nn.init.normal_(self.beat_embedding.weight)
        self.tick_embedding = nn.Embedding(
            vocab.quantize_divisions, params.context_embedding_dim
        )
        torch.nn.init.normal_(self.tick_embedding.weight)
        self.polyphony_embedding = nn.Embedding(
            vocab.embed_polyphony_max + 1, params.context_embedding_dim
        )
        torch.nn.init.normal_(self.polyphony_embedding.weight)
        self.context_mix = nn.Linear(
            params.context_embedding_dim * 8, params.dim, bias=False
        )
        torch.nn.init.normal_(
            self.context_mix.weight, std=1 / math.sqrt(params.context_embedding_dim * 8)
        )
        self.context_norm = RMSNorm(params.dim)

        self.param_count = 0
        for p in self.parameters():
            self.param_count += p.numel()

    def get_device(self):
        return next(self.parameters()).device

    def reallocate_caches(self):
        self.transformer.reallocate_caches()

    def hint_inference_batch_size(self, batch_size):
        pass

    def rearrange(self, idxs):
        self.transformer.rearrange(idxs)

    def train(self, *args):
        super().train(*args)
        if self.encoder is not None:
            self.encoder.eval()

    def forward(
        self,
        x,
        start_pos=0,
        return_embeddings=False,
        encoder_input_ids=None,
        encoder_attention_mask=None,
    ):
        # for compatibility with accelerated model
        if x.device.type != "cuda":
            x = x.to(self.get_device())
        if encoder_input_ids is not None and encoder_input_ids.device.type != "cuda":
            encoder_input_ids = encoder_input_ids.to(self.get_device())
        if (
            encoder_attention_mask is not None
            and encoder_attention_mask.device.type != "cuda"
        ):
            encoder_attention_mask = encoder_attention_mask.to(self.get_device())

        assert start_pos > 0 or ((self.encoder is None) == (encoder_input_ids is None))
        if encoder_attention_mask is not None:
            assert self.encoder is not None
        context = self.context_mix(
            torch.cat(
                [
                    self.bar_embedding(x[..., 1]),
                    self.beat_embedding(x[..., 2]),
                    self.tick_embedding(x[..., 3]),
                    self.polyphony_embedding(x[..., 4]),
                    self.bar_embedding(x[..., 5]),
                    self.beat_embedding(x[..., 6]),
                    self.tick_embedding(x[..., 7]),
                    self.polyphony_embedding(x[..., 8]),
                ],
                dim=-1,
            )
        )
        context = self.context_norm(context)

        if start_pos == 0:
            with torch.no_grad():
                e = (
                    self.encoder(encoder_input_ids, encoder_attention_mask)
                    if encoder_input_ids is not None
                    else None
                )
        else:
            e = None

        h = self.transformer(
            x[..., 0],
            start_pos=start_pos,
            aux_embeddings=context,
            return_embeddings=return_embeddings,
            encoder_out=e,
            encoder_out_mask=encoder_attention_mask,
        )
        if return_embeddings:
            # average pool over sequence length of non-pad tokens and return token counts
            # XXX this no longer matches the accelerated model behavior
            tok_mask = x[..., 0].ne(self.params.padding_idx)
            tok_counts = tok_mask.sum(dim=-1)
            return (
                torch.where(tok_mask.unsqueeze(-1), h, 0).sum(dim=1)
                / (tok_counts.unsqueeze(-1).to(h.dtype) + 1e-6),
                tok_counts,
            )
        return h
