AdaptiveLogSoftmaxWithLoss returns a loss, so you do not need another NLLoss. I never used that one, but I assume you need to change your forward() method to
def forward(self, x, targets):
        # Set initial states
        h0 = torch.zeros(self.num_layers*2, x.size(0), self.hidden_size)
        c0 = torch.zeros(self.num_layers*2, x.size(0), self.hidden_size)

        # Forward propagate LSTM
        embed = nn.Embedding(vocab_size, emb_size)
        out, _ = self.blstm(embed(x), (h0, c0))  # out: tensor of shape (batch_size, seq_length, hidden_size*2)
        
        # Decode the hidden state of the last time step
        out = self.fc(out[:, -1, :], targets)       
        return out

Note that out is
NamedTuple`` with ``output`` and ``loss`` fields:
            * **output** is a Tensor of size ``N`` containing computed target
              log probabilities for each example
            * **loss** is a Scalar representing the computed negative
              log likelihood loss

And you need to change your training loop to
# Forward pass
outputs = model(inputs, targets)
outputs = outputs.to(device) # the loss is "somehwere" in the outputs tuple
#loss = criterion(outputs, targets)
#print(loss.item())