I can’t spot any obvious error or a line of code, which might detach the tensors from the computation graph.
Just to make sure, you are indeed not detaching, you could print the gradients of all parameters after the backward pass:
for name, param in encoder.named_parameters():
    print(name, param.grad)
# same for decoder

If you see some None outputs, this would mean that these parameters won’t get gradients and thus won’t be updated.