I see! Thank you so much for pointing this out!
Even when I declare an optimizer to step through like your suggestion above, the loss does not change. Is there a common step I am forgetting here?
import torch
torch.autograd.set_detect_anomaly(True)

N=15
# load into pytorch
D = torch.rand(N, N).float()
H = torch.eye(N).float() - \
    1 / N * torch.ones(D.shape).float()
B = 0.5 * torch.matmul(torch.matmul(H, D ** 2), H)
# pts = torch.rand(N, 3).float()

# declare constants
stop_loss = 1e-2
step_size = stop_loss / 3

# # emulate simulate SVD
# U = torch.rand(N, N, requires_grad=True)
# s = torch.diag(torch.sort(torch.rand(N), descending=True).values).requires_grad_()
# Vh = torch.rand(N, N, requires_grad=True)

optimizer = torch.optim.SGD([U,s,Vh], lr=step_size)
# find embedding
# embed = torch.matmul(torch.sqrt(s), Vh)
# X_hat = embed.T[:, :3]  # select the first 3 coordinates --> x, y, z

for i in range(100000):

    # calculate loss1: how close is our SVD function?
    delta1 = (torch.matmul(U.T, U) - torch.eye(N)) + \
             (torch.matmul(Vh.T, Vh) - torch.eye(N)) + \
             (torch.matmul(torch.matmul(U, s), Vh.T) - B)

    L1 = torch.norm(delta1, p=2)

    # calculate loss2: how close is our MDS approximation?
    # # delta2 = torch.nan_to_num(X_hat - pts, nan=0.0)

    # # L2 = torch.norm(delta2, p=2)

    # Backprop
    loss = L1
    loss.backward()

    # update
    optimizer.zero_grad()
    optimizer.step()

    if i % 1000 == 0:
        print('Loss is %s at iteration %i' % (loss, i))

    if abs(loss) < stop_loss:
        break

Output:
Loss is tensor(141.6570, grad_fn=<NormBackward1>) at iteration 0
Loss is tensor(141.6570, grad_fn=<NormBackward1>) at iteration 1000
Loss is tensor(141.6570, grad_fn=<NormBackward1>) at iteration 2000
Loss is tensor(141.6570, grad_fn=<NormBackward1>) at iteration 3000
Loss is tensor(141.6570, grad_fn=<NormBackward1>) at iteration 4000
...
...
etc.