How to try/catch errors during training

SingleShockControl: <p>Hi there!</p>
<p>The situation is as follows. I have a pytorch training loop with roughly the following structure:</p>
<pre><code class="lang-auto">optimizer = get_opt()
train_data_loader = Dataloader()
net = get_model()
for epoch in range(epochs):
    for batch in train_data_loader:
        output = net(batch)
        output["loss"].backward()
        optimizer.step()
        optimizer.zero_grad()
</code></pre>
<p>My dataset contains images and is very large (in number of items) and takes about half a day per epoch. Now some samples in my dataset can be of bad quality.<br>
I want my training loop therefore to catch errors that incur during the forward pass, log them, and continue training, instead of interrupting the whole program because of some bad data element. This is what I had in mind:</p>
<pre><code class="lang-auto">optimizer = get_opt()
train_data_loader = Dataloader()
net = get_model()
for epoch in range(epochs):
    for batch in train_data_loader:
        try:
            output = net(batch)
        except Exception as e:
             logging.error(e, exc_info=True)  # log stack trace
             continue
        output["loss"].backward()
        optimizer.step()
        optimizer.zero_grad()
</code></pre>
<p>This way, if a forward pass fails, it will just get the next batch and not interrupt training. This works great for the validation loop, but during training I run into problems: GPU memory will not be released after the try/catch, and so I run into an OOM when pytorch tries to put the next batch on the GPU.<br>
Things I’ve tried: after an error is catched (i.e. within the <code>except</code>):</p>
<ul>
<li>move every parameter in <code>net.parameters()</code> to cpu, and/or detach them</li>
<li>delete every parameter in <code>net.parameters()</code>
</li>
<li>run <code>torch.cuda.empty_cache()</code>
</li>
<li>manually run <code>gc.collect()</code> (python garbage collection)</li>
<li>manually set all gradients of every parameter in <code>net.parameters()</code> to <code>None</code>
</li>
</ul>
<p>None of these methods made any difference. Is there a way to do this? I suspect its the gradients/ graph not being cleared, as this problem does not happen during validation (e.g. within a <code>with torch.no_grad()</code> context).</p>

albanD: <p>Hi,</p>
<p>I think it is a “known” issue with python exceptions. See <a href="https://cosmicpercolator.com/2016/01/13/exception-leaks-in-python-2-and-3/" class="inline-onebox">Exception leaks in Python 2 and 3 | Kristján's Cosmic Percolator</a><br>
In your case, since the differentiable output is in the current frame, it is kept alive by the exception as so holds on to the GPU memory forever since you never exit the function.</p>
<p>I think a simple fix for you would be to move the content of the loop in a separate function that will be exited at every iteration (clearing the exception properly).</p>

SingleShockControl: <p>Thanks for the information! I learned quite a bit from that blog post, so thanks for that.<br>
The issue is however still not resolved. I’ve tried:</p>
<ul>
<li>putting the try except in a dedicated function</li>
</ul>
<pre><code class="lang-auto">def try_catch_forward(batch, net, optimizer):
   try:
       output = net(batch)
       loss = output["loss"]
       loss.backward()
       optimizer.step()
       optimizer.zero_grad()
   except Exception as e:
       logger.error(e, exc_info=True)
       loss = np.zeros((1))
   gc.collect()
   torch.cuda.empty_cache()
   return loss
</code></pre>
<ul>
<li>manually clearing exception traceback with <code>e.__traceback__= None</code> (in the dedicated function)</li>
<li>doing <code>traceback.clear_frames(e.__traceback__)</code>
</li>
<li>not logging the exception at all</li>
<li>
<code>gc.collect()</code> and <code>torch.cuda.empty_cache()</code> inside the dedicated try/catch function.</li>
</ul>
<p>None worked, unfortunately.</p>

albanD: <p>Note that the <code>empty_cache()</code> is not really needed as if you are about to run out of memory, it will be called automatically. And calling it too much is going to slow down your code quite a bit.</p>
<p>How do you measure if the issue is “resolved” or not?<br>
Is there a small code sample (30-40 lines) you could share to see if we can reproduce it?</p>

SingleShockControl: <p>I set up the most basic network and data loader to reproduce the problem (74 lines sorry)</p>
<pre><code class="lang-auto">import torch
from torch import nn
import logging

logger = logging.getLogger(__name__)
logging.basicConfig()
logger.setLevel(logging.INFO)
NUM_CLASSES = 10


class Net(nn.Module):
    def __init__(self,):
        super(Net, self).__init__()
        self.network = torch.hub.load(
            "rwightman/pytorch-image-models",
            "efficientnet_b0",
            pretrained=True,
            features_only=True,
            in_chans=3,
        )
        mid_ch = self.network.feature_info[-1]["num_chs"]
        self.classifier = nn.Conv2d(mid_ch, NUM_CLASSES, kernel_size=1)
        self.criterion = nn.CrossEntropyLoss()

    def forward(self, batch):
        batch["image"] = batch["image"].float().cuda()
        batch["gt"] = batch["gt"].cuda()
        x = self.network(batch["image"])[-1]
        out_size = batch["gt"].size()[-2:]
        logits = self.classifier(x)
        logits = nn.functional.interpolate(
            logits,
            size=out_size,
            mode="bilinear",
            align_corners=False,
            recompute_scale_factor=False,
        )
        loss = self.criterion(target=batch["gt"].long().squeeze(1), input=logits)
        return {"loss": loss}


class Dataloader:
    def __init__(self, batch_size, size):
        self.batch_size = batch_size
        self.size = size

    def __iter__(self,):
        for i in range(250):
            img = torch.randn((self.batch_size, 3, self.size, self.size))
            target = torch.randint(NUM_CLASSES, (self.batch_size, 1, self.size, self.size))
            batch = {"image": img, "gt": target}
            yield batch


net = Net()
net.cuda()
optimizer = torch.optim.SGD(
    net.parameters(), lr=1e-2, weight_decay=5e-4, momentum=0.9, nesterov=False,
)
train_data_loader = Dataloader(batch_size=6, size=512)
logger.warning('starting training')
for i, batch in enumerate(train_data_loader):
    try:
        output = net(batch)
        if i % 50 == 0 and i != 0:
            raise ValueError
    except Exception as e:
        logging.error(e, exc_info=True)  # log stack trace
        continue
    logger.info(f'Loss: {output["loss"].item()} | iter: {i}')
    output["loss"].backward()
    optimizer.step()
    optimizer.zero_grad()

</code></pre>
<p>What is interesting is that the problem only occurs when the gpu memory consumption is well over 50% (for me that was at batch size 7 at minimum, with a 1070ti w/ ~8Gb memory), but the problem did not occur when using a batch size of 6 for instance. Here is a gpu consumption graph, before the first error, and after 2 errors when the problem does <strong>not</strong> occur, using a batch size of 6:</p>
<p><img src="https://discuss.pytorch.org/uploads/default/original/3X/b/1/b188382c117d36b4edba427305aceb2d9e181809.png" alt="error" data-base62-sha1="pkwmnOZU7UqS8kFosLLQsp41fjr" width="550" height="366"><br>
(yellow is gpu memory, blue gpu util)<br>
So clearly <em>some</em> extra memory is allocated (to what I don’t know) due to the catched error, but it does not accumulate further beyond that, when another error occurs…<br>
I have also verified that <strong>no</strong> gpu memory accumulates on catched errors when setting the whole loop in a <code>with torch.no_grad()</code> context, so it must have something to do with autograd right?</p>
<p>For me this issue is resolved in one of two ways:</p>
<ul>
<li>I am shown a way that I can catch errors without gpu memory consumption building up (i.e. clearing the autograd graph somehow manually?)</li>
<li>or when this is simply not possible currently <img src="https://discuss.pytorch.org/images/emoji/apple/man_shrugging.png?v=12" title=":man_shrugging:" class="emoji" alt=":man_shrugging:" loading="lazy" width="20" height="20"> although I can’t imagine I’m the only one with this use case though, so then I’ll open a feature request in the pytorch repository.</li>
</ul>

albanD: <p>Your code sample runs just fine on colab <img src="https://discuss.pytorch.org/images/emoji/apple/confused.png?v=9" title=":confused:" class="emoji" alt=":confused:"></p>
<p>Also the fact that the memory reported by the OS changed is expected. We use a special allocator that does not return the memory to the OS as soon as it is freed to improve speed.<br>
So the bump here could be due to the change of allocation pattern after the error. But no further increase after that because the pattern remains the same.</p>

SingleShockControl: <blockquote>
<p>Your code sample runs just fine on colab <img src="https://discuss.pytorch.org/images/emoji/apple/confused.png?v=12" title=":confused:" class="emoji" alt=":confused:" loading="lazy" width="20" height="20"></p>
</blockquote>
<p>Yeah no the sample runs depending on your gpu capabilities/ memory. I recommend scaling up the batch_size (in line <code>train_data_loader = Dataloader(batch_size=6, size=512)</code> to a higher one until you encounter the problem (so that the first 50 iterations run without problems but then a Cuda OOM is thrown for iter &gt; 50)</p>
<blockquote>
<p>Also the fact that the memory reported by the OS changed is expected. We use a special allocator that does not return the memory to the OS as soon as it is freed to improve speed.<br>
So the bump here could be due to the change of allocation pattern after the error. But no further increase after that because the pattern remains the same.</p>
</blockquote>
<p>So you’re saying that some extra memory is allocated to the autograd graph gradients that is never released? And cannot be released in any way?</p>

albanD: <aside class="quote no-group" data-username="SingleShockControl" data-post="7" data-topic="108619">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt="" width="24" height="24" src="https://discuss.pytorch.org/letter_avatar_proxy/v4/letter/s/b77776/48.png" class="avatar"> SingleShockControl:</div>
<blockquote>
<p>So you’re saying that some extra memory is allocated to the autograd graph gradients that is never released? And cannot be released in any way?</p>
</blockquote>
</aside>
<p>Some extra memory is allocated during forward to be able to compute the backward yes.<br>
This memory is released when all the references to it are dead (in general when your output/loss goes out of scope).<br>
On top of that, the GPU memory is cached and is not returned to the OS even though it is available to allocate more Tensors within pytorch.</p>

SingleShockControl: <p>So basically so solve this, I’d have to somehow get the latest “node” in the forward pass before the error occurred, compute a loss with that, and do <code>loss.backward(); optimizer.zero_grad()</code> to make its allocated memory available again for the next batch.</p>
<p>Or always keep half of my GPU memory available for potential “error space” <img src="https://discuss.pytorch.org/images/emoji/apple/stuck_out_tongue.png?v=12" title=":stuck_out_tongue:" class="emoji" alt=":stuck_out_tongue:" loading="lazy" width="20" height="20"></p>
<blockquote>
<p>On top of that, the GPU memory is cached and is not returned to the OS even though it is available to allocate more Tensors within pytorch.</p>
</blockquote>
<p>Ah right.</p>

albanD: <aside class="quote no-group" data-username="SingleShockControl" data-post="9" data-topic="108619">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt="" width="24" height="24" src="https://discuss.pytorch.org/letter_avatar_proxy/v4/letter/s/b77776/48.png" class="avatar"> SingleShockControl:</div>
<blockquote>
<p>Or always keep half of my GPU memory available for potential “error space” <img src="https://discuss.pytorch.org/images/emoji/apple/stuck_out_tongue.png?v=12" title=":stuck_out_tongue:" class="emoji" alt=":stuck_out_tongue:" loading="lazy" width="20" height="20"></p>
</blockquote>
</aside>
<p>No, you don’t have to, because even though it looks used when looking from the OS point of view, it is actually available to allocate more Tensors.<br>
You can consider this a bad case scenario for the allocator that doesn’t realize you’re doing the same thing and so is caching more memory (even though it has a lot of available memory already).</p>

SingleShockControl: <p>Ah okay. So in short there is no way to do what I want to do; catch and log errors happening in the forward pass during model training, without interrupting training, and while fully utilizing the GPU memory. I.e. there is no way to avoid the extra GPU memory allocation that happens when an error is catched.</p>
<p>Is this something that could be fixed by posting an issue in the pytorch repository?</p>

albanD: <p>I am still not sure what the issue is here.<br>
Nothing in PyTorch is holding onto that memory so there isn’t much we can do I think. It is most likely something in python when the exception is handled. So I would look more into that.<br>
The extra memory use from the OS point of view is most likely a side effect of the caching alocator but the actual used memory is most likely the same.</p>
<p>I don’t think opening an issue would be very helpful if we don’t know where it comes from.</p>

SingleShockControl: <p>Hmm. The issue is that, if a forward pass is interrupted by an error, but the error is not raised, the following forward pass will require <em>additional</em> GPU memory, potentially causing an OOM (if the GPU memory is already almost fully used before the error happens).<br>
In other words, there is no way nicely recover from an error incurred in the forward pass during training, which would be a nice feature.</p>
<p>So just to be clear, you are saying that additional GPU memory is allocated but not actually used?<br>
Were you able to reproduce the problem using a higher batch size?</p>
<p>Thanks for your answers so far by the way, and sorry for my limited understanding</p>

albanD: <p>Not really.<br>
See the modified code here that prints the actual used memory accros iterations:</p>
<pre><code class="lang-python">import torch
from torch import nn
import logging

logger = logging.getLogger(__name__)
logging.basicConfig()
logger.setLevel(logging.INFO)
NUM_CLASSES = 10


class Net(nn.Module):
    def __init__(self,):
        super(Net, self).__init__()
        self.network = torch.hub.load(
            "rwightman/pytorch-image-models",
            "efficientnet_b0",
            pretrained=True,
            features_only=True,
            in_chans=3,
        )
        mid_ch = self.network.feature_info[-1]["num_chs"]
        self.classifier = nn.Conv2d(mid_ch, NUM_CLASSES, kernel_size=1)
        self.criterion = nn.CrossEntropyLoss()

    def forward(self, batch):
        batch["image"] = batch["image"].float().cuda()
        batch["gt"] = batch["gt"].cuda()
        x = self.network(batch["image"])[-1]
        out_size = batch["gt"].size()[-2:]
        logits = self.classifier(x)
        logits = nn.functional.interpolate(
            logits,
            size=out_size,
            mode="bilinear",
            align_corners=False,
            recompute_scale_factor=False,
        )
        loss = self.criterion(target=batch["gt"].long().squeeze(1), input=logits)
        return {"loss": loss}


class Dataloader:
    def __init__(self, batch_size, size):
        self.batch_size = batch_size
        self.size = size

    def __iter__(self,):
        for i in range(10):
            img = torch.randn((self.batch_size, 3, self.size, self.size))
            target = torch.randint(NUM_CLASSES, (self.batch_size, 1, self.size, self.size))
            batch = {"image": img, "gt": target}
            yield batch


net = Net()
net.cuda()
optimizer = torch.optim.SGD(
    net.parameters(), lr=1e-2, weight_decay=5e-4, momentum=0.9, nesterov=False,
)
train_data_loader = Dataloader(batch_size=10, size=40)
logger.warning('starting training')
for i, batch in enumerate(train_data_loader):
    print(torch.cuda.memory_allocated())
    try:
        output = net(batch)
        print(torch.cuda.memory_allocated())
        if i % 3 == 0 and i != 0:
            raise ValueError("Value error")
    except Exception as e:
        # logging.error(e, exc_info=True)  # log stack trace
        print('caugh an error, keep going ' + str(e))
        continue
    print(f'Loss: {output["loss"].item()} | iter: {i}')
    output["loss"].backward()
    optimizer.step()
    optimizer.zero_grad()
</code></pre>
<p>Which prints:</p>
<pre><code class="lang-auto">WARNING:__main__:starting training

14631936
51151872
Loss: 3.2388575077056885 | iter: 0
43490816
80133120
Loss: 3.1496763229370117 | iter: 1
43490816
80133120
Loss: 3.0223429203033447 | iter: 2
43490816
80133120
caugh an error, keep going Value error
80133120
80010240
Loss: 3.105882167816162 | iter: 4
43490816
80133120
Loss: 3.029092311859131 | iter: 5
43490816
80133120
caugh an error, keep going Value error
80133120
80010240
Loss: 2.8809430599212646 | iter: 7
43490816
80133120
Loss: 2.9971139430999756 | iter: 8
43490816
80133120
caugh an error, keep going Value error

</code></pre>
<p>Most iteration, the memory goes down because the call to backward frees the buffers.<br>
When you skip that because of the error, the memory is not freed but that does not change the final usage.</p>

SingleShockControl: <p>Thanks for this. So the backward call frees the buffers for the next batch. As we skip the backward call this will raise the memory usage one iteration basically, leading to an OOM if no extra GPU space is available. Is there a way to manually clear those buffers without calling backward?<br>
From your answer on <a href="https://discuss.pytorch.org/t/how-to-free-graph-manually/9255/2">this post</a>, the only option I see is getting the last tensor in the graph before the error occurred (somehow) and deleting it (or calling backward on it and setting al gradients to zero).</p>

albanD: <blockquote>
<p>Is there a way to manually clear those buffers without calling backward?</p>
</blockquote>
<p>Yes you just need to make sure you don’t have any reference to this graph before looping.<br>
You can either do a bunch of <code>del</code> statements. Or use a function so that you are sure that everything that was still in scope is deleted before looping.</p>