The gradients will be all zero, since output-output creates a zero tensor and ftn is detached from the computation graph using the str transformations.
The loss calculation should depend on the actual computation graph, i.e. the model prediction should be involved in the calculation and not cancel out itself.
Your current models is basically: linear -> ReLU -> ReLU.
If you just want a single linear layer without a non-linearity, you could just use:
model = nn.Linear(n_in, n_out)

Here is a simple training routine learning just constant values:
model = nn.Linear(n_in, n_out)
x = torch.randn(batch_size, n_in)
y = torch.tensor([[100.0, 100.0, 100.0]]*batch_size)
optimizer = torch.optim.Adam(model.parameters(), lr=1,)

for epoch in range(200):
    optimizer.zero_grad()
    output = model(x)
    loss = F.mse_loss(output, y)
    loss.backward()
    optimizer.step()
    print('Epoch {}, loss {}'.format(
        epoch, loss.item()))

print('output', output)
> output tensor([[100.0103, 100.0111, 100.0113],
        [100.0012, 100.0013, 100.0013],
        [100.0001,  99.9994,  99.9993],
        [100.0003, 100.0002, 100.0002],
        [ 99.9976,  99.9974,  99.9973]], grad_fn=<AddmmBackward>)

Let me know, how your custom loss function should work and we could try to adapt the code to fit your use case.