I think I’ve found the issue. The problem was with the choice of optimizer. Switching from sgd to adam seems to work. Including the changes for reference.
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.autograd import Variable
import torch.optim as optim

import matplotlib.pyplot as plt
from tqdm import tqdm

np.set_printoptions(precision=4, linewidth=500, suppress=True)

class Example(nn.Module):
    def __init__(self, D_in, D_out, total):
        super(Example, self).__init__()
        self.syn0 = nn.Linear(D_in,total) 
        self.syn1 = nn.Linear(total,D_out) 

    def forward(self, x):
        x = F.sigmoid(self.syn0(x))
        x = F.sigmoid(self.syn1(x))
        return x

X_np = np.array([[0,0,1],
                 [0,1,1],
                 [1,0,1],
                 [1,1,1]])
X = Variable(torch.Tensor(X_np), requires_grad=True)
y = np.array([[0],
            [1],
            [1],
            [0]])
y = Variable(torch.Tensor(y))

model = Example(3,1,4) #in, out, total

# optimizer = torch.optim.SGD(model.parameters(), lr=0.001) # !!! 
optimizer = optim.Adam(model.parameters(), lr=0.001)

losses = []
for t in tqdm(range(10000)):
    y_pred = model(X)
    loss = (y_pred - y).pow(2).sum()
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    losses.append(loss.data.numpy())

plt.plot(losses)
plt.show()
    
predicted = model.forward(Variable(torch.from_numpy(X_np).float())).data.numpy()
print '\n', predicted