Let us say I am implementing an autoencoder. My source and target are going to be same, let us say I want to reconstruct some vector x.

So something like
z = Encoder(x)
y = Decoder(z)

loss = LOSS(y, x)

loss = LOSS(y, x.clone())

loss = LOSS(y, x.clone().detach())

What is the difference in behavior in each of the cases, x is of course a leaf tensor, but I don’t understand the gradients aspect is it 1) going to form a cycle ?

It shouldn’t be a problem, as usually x won’t require gradients in an autoencoder setup.
The first two approaches should yield the same result, while the last one should be different.
Here is a small dummy code:

x = torch.randn(1, requires_grad=True)
param = nn.Parameter(torch.randn(1))
output = x * param
# 1.
loss = F.mse_loss(output, x)
loss.backward()
print(param.grad)
print(x.grad)
x.grad.zero_()
param.grad.zero_()
# 2.
output = x * param
loss = F.mse_loss(output, x.clone())
loss.backward()
print(param.grad)
print(x.grad)
x.grad.zero_()
param.grad.zero_()
# 3.
output = x * param
loss = F.mse_loss(output, x.clone().detach())
loss.backward()
print(param.grad)
print(x.grad)
> tensor([-3.3104])
tensor([0.6608])
> tensor([-3.3104])
tensor([0.6608])
> tensor([-3.3104])
tensor([-0.9747])

Usually you don’t need gradients in your input tensor.
However there are some use cases, e.g. in adversarial training, where these gradients can be used for small perturbation of the input.