Losing gradients when using auxiliary model to compute loss

username: vanto
cooked: <h3>
<a name="overview-1" class="anchor" href="#overview-1"></a>Overview</h3>
<p>I am trying to train a model that can have a rather complex loss function that does operations using the output of the model. The easiest way to do these operations is to use the internal machinery of a second model to compute the loss. This causes the pytorch to lose the gradients.</p>
<p>Is there a way to have pytorch track the gradients of the model when the loss uses an auxiliary model to do the loss computations?</p>
<h3>
<a name="minimal-example-2" class="anchor" href="#minimal-example-2"></a>Minimal Example</h3>
<p>I’ve made a MWE using just a basic linear models to illustrate the problem. In the example below, L1 represents the model I am actually interested in training and L2 represents the secondary model I’d like to use to compute the loss. In my actual use case, I am not at all interested in training L2, it’s just there to be used to compute the loss. It would also be cumbersome and presumably computationally slower to reimplement the inner workings of the secondary model.</p>
<p>Is there a way to get pytorch to track the gradients of L1 when using the output in L2?</p>
<pre><code class="lang-auto"># data
X1 = torch.tensor([[.5]])
X2 = torch.arange(1, 11.)
y = 2*X2
if len(X2.shape) == 1: # if X is a 1d tensor make 2d
    X2.unsqueeze_(1) # shape is now n by 1

# create a basic model
class Linear(nn.Module):
    def __init__(self):
        super().__init__()
        self.l = skip_init(nn.Linear, 1, 1, bias = False)

    def forward(self, x):
        x = self.l(x)
        return x

#  loss that uses a secondary model to do operations
def wrapped_loss(L, X = X2, y = y):
    Y = y.unsqueeze(1)
    d = (L(X)-Y).pow(2)
    return d.mean()

# loss that does the operations with the output itself
def loss(b, X = X2, y = y):
    Y = y.unsqueeze(1)
    d = (torch.mm(X, b) - Y).pow(2)
    return d.mean()

# initializations 
L1 = Linear() # thing we are interested in updating
L1.l.weight = nn.Parameter(torch.tensor([[4.]]))
L2 = Linear() # auxilary computation, the parameters will be populated from output that depends on L1
opt = torch.optim.SGD(params = L1.parameters(), lr = .1)
epochs = 1

# training
for epoch in range(epochs):   
    intermediate_output = 2*L1(X1)
    
    # put output into the secondary model that will 
    # be used to do operations with the output.
    with torch.no_grad():
        L2.l.weight.copy_(intermediate_output) 

    wrapped_l = wrapped_loss(L2) # this is identical to l = loss(intermediate_output)

    opt.zero_grad()
    wrapped_l.backward() # compare against using l.backward()
    opt.step()

    print(L1.l.weight.grad) # this is currently None but we'd expect it to be the same as when we use loss

</code></pre>

username: ptrblck
cooked: <p>The tensor is detached since you are copying the intermediate activation into the <code>weight</code> in a <code>no_grad</code> context.<br>
<a href="https://pytorch.org/docs/stable/generated/torch.func.functional_call.html#torch.func.functional_call"><code>torch.func.functional_call</code></a> should work as seen here:</p>
<pre><code class="lang-python"># create a basic model
class Linear(nn.Module):
    def __init__(self):
        super().__init__()
        self.l = nn.Linear(1, 1, bias = False)

    def forward(self, x):
        x = self.l(x)
        return x

#  loss that uses a secondary model to do operations
def wrapped_loss(L, intermediate_output, X, y):
    Y = y.unsqueeze(1)
    d = (torch.func.functional_call(L.l, {"weight": intermediate_output}, X)-Y).pow(2)
    return d.mean()


# data
X1 = torch.tensor([[.5]])
X2 = torch.arange(1, 11.)
y = 2*X2
if len(X2.shape) == 1:
    X2.unsqueeze_(1)

# initializations 
L1 = Linear() # thing we are interested in updating
L1.l.weight = nn.Parameter(torch.tensor([[4.]]))
L2 = Linear() # auxilary computation, the parameters will be populated from output that depends on L1
opt = torch.optim.SGD(params = L1.parameters(), lr = .1)
epochs = 5

# training
for epoch in range(epochs):   
    intermediate_output = 2*L1(X1)
    wrapped_l = wrapped_loss(L2, intermediate_output, X2, y)

    opt.zero_grad()
    wrapped_l.backward()
    opt.step()

    print(L1.l.weight.grad)
    
# tensor([[154.]])
# tensor([[-1031.8000]])
# tensor([[6913.0605]])
# tensor([[-46317.5078]])
# tensor([[310327.3125]])
</code></pre>

username: travellingbones
cooked: <p>Is there a way to use your solution (calling <code>torch.funct.function_call(L2, params_dict, X2)</code>) over a <em>set</em> of parameters? In my case, instead of one set of parameters <code>{"weight": intermediate_output}</code> the loss depends on computations using the machinery of <code>L2</code> model instantiated with <code>m</code> different sets of parameters: <code>list_of_params = [{"weight": intermediate_output_1}, ..., {"weight": intermediate_output_m}]</code>.</p>
<p>The naive solution is to use a for loop:</p>
<pre><code>losses = torch.tensor([(torch.func.functional_call(L2, param_dict, X2)-Y2).pow(2) for param_dict in list_of_params ])
loss = losses.mean()
</code></pre>
<p>I’m guessing it is computationally inefficient, but likely there is some vectorized version for doing this? Like a map reduce?</p>
<p>Thanks! this thread is very helpful!</p>

username: ptrblck
cooked: <p><code>functional_call</code> accepts multiple directories as seen in the example in the <a href="https://pytorch.org/docs/stable/generated/torch.func.functional_call.html?highlight=functional_call#torch.func.functional_call">docs</a> so did you try to use this approach?</p>

username: travellingbones
cooked: <p>Yes. Here are three approaches/examples and the errors thrown by them, but first, the “background” notation.</p>
<p><code>M2</code> is a simple logistic regression model (class <code>nn.Module</code>), and it has the following state dict format:<br>
<code>{	'linear.weight': &lt;2 by 1 tensor&gt; ,'linear.bias': &lt;1 by 1 tensor&gt; }</code><br>
In short it has 3 parameters.</p>
<p>Now I’ll make a list of parameter dictionaries. These are from the outputs of a different model <code>M1</code>. I need the M2 model to be evaluated on dataset <code>X</code> for each set of parameters in the list, in order to compute the loss function for training <code>M1</code> (so gradients of <code>M1</code> need to be tracked through the computation)</p>
<p>Output of <code>M1</code> provides torch tensor <code>B</code> which is 2 by 3 (two sets of parameters for <code>M2</code>).<br>
e.g.:</p>
<pre><code class="lang-auto">B = torch.tensor([[1.,2,3], [4,5,6] ]) # output of M1

param_dicts = [ { 'linear.weight': B[0,:-1].unsqueeze(0), 'linear.bias': B[0,-1:] },
                { 'linear.weight': B[1,:-1].unsqueeze(0), 'linear.bias': B[1,-1:] } ]
</code></pre>
<hr>
<p>Errant try 1:<br>
Now trying to pass the list to <code>function_call</code> fails:<br>
<code>x = functional_call(M2, param_dicts, X)</code># this fails with error “ValueError: [‘linear.weight’, ‘linear.bias’] appeared in multiple dictionaries; behavior of functional call is ambiguous”:</p>
<hr>
<p>Errant try 2:<br>
Trying to use the for loop inside the list works partially–it computes M2(params, X), but it doesn’t track M1’s gradients:</p>
<pre><code class="lang-auto">opt = torch.optim.RMSprop( params = M1.parameters(), lr = learning_rate, momentum = momentum)

x = torch.tensor([ ((torch.func.functional_call(M2, d, X) - Y).pow(2)).mean() for d in param_dicts])  # no errors!
loss = x.mean() 

# now step M1's params based on that computation... error:
opt.zero_grad()
loss.backward() # &lt;--- error here
opt.step()
scheduler.step(loss)
</code></pre>
<p>Error given is “RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn”</p>
<hr>
<p>Errant try 3: use the <code>grad</code> function: <a href="https://pytorch.org/docs/stable/generated/torch.func.grad.html" class="inline-onebox" rel="noopener nofollow ugc">torch.func.grad — PyTorch 2.0 documentation</a> to explicitly track the gradients of M1?</p>
<pre><code class="lang-auto">def compute_loss(m1_state_dict): 
    global X # training features for M2
    global Y # training targets for M2
    global M1 # first model 
    global M2
    global batch_size
    global dim 
    samples = random_normal_samples(batch_size, dim) # input data for the NF

    B, log_det_T = functional_call(M1,m1_state_dict,samples)   # do i need to use func.function_call here? I want to track M1's gradients,
    param_dicts = [ {	'linear.weight': B[i,:-1].unsqueeze(0), 'linear.bias': B[i,-1:]}\
            for i in range(B.shape[0])]
    l2_losses = torch.tensor([ ((torch.func.functional_call(M2, d, X) - Y).pow(2)).mean()\
                                for d in param_dicts]) # shape = [batchsize]    
    loss = (-l2_losses + log_det_T.squeeze()).mean()
    return loss

m1_state_dict = M1.state_dict()
grad(compute_loss)(m1_state_dict) # &lt; --- error here: 
---
RuntimeError: unwrapped_count &gt; 0 INTERNAL ASSERT FAILED at "/Users/runner/work/pytorch/pytorch/pytorch/aten/src/ATen/functorch/TensorWrapper.cpp":202, please report a bug to PyTorch. Should have at least one dead wrapper

</code></pre>
<p>Overall, I need to train M1, with loss that depends on M1’s output and M2 using some of M1’s output as parameters, and I’m stuck. Thanks!</p>

username: travellingbones
cooked: <p><a class="mention" href="/u/ptrblck">@ptrblck</a> should i create a new question for this? I’m unsure if I should expect a response here or need to start a new thread.</p>