Curious about something: it seems odd that the attention weights are calculated without looking at encoder_output. Relevant line is:
attn_weights = F.softmax(self.attn(torch.cat((embedded[0], hidden[0]), 1)), dim=1)

This seems weird – do you have an intuition on why this would work? I thought you had to look at the encoder when calculating weights (your newer tutorial versions do use the encoder_outputs as I would expect, btw)