TensorBoard's add_histogram() execution time explodes with each epoch

username: Andry_Bal
cooked: <p>Hi Everyone,<br>
I am using TensorBoard in my PyTorch project and I faced a problem. After each training epoch I log histograms of model parameters using <em>SummaryWriter.add_histogram()</em>. When I set <em>weight_decay=0</em> in my optimizer everything works as expected (see left image below). However, when I set weight decay to some non-zero value, e.g. <em>weight_decay=0.1</em>, the histogram writing time starts to explode with every epoch (right image), rendering training impossible. Along with that, all my RAM is being filled.<br>
<div class="lightbox-wrapper"><a class="lightbox" href="https://discuss.pytorch.org/uploads/default/original/3X/e/d/edc387377eea358b50f6348429447324bc700198.jpeg" data-download-href="https://discuss.pytorch.org/uploads/default/edc387377eea358b50f6348429447324bc700198" title="collage"><img src="https://discuss.pytorch.org/uploads/default/optimized/3X/e/d/edc387377eea358b50f6348429447324bc700198_2_517x360.jpeg" alt="collage" data-base62-sha1="xVm4OJ6dzzUEhWYg54hNXxBpGdy" width="517" height="360" srcset="https://discuss.pytorch.org/uploads/default/optimized/3X/e/d/edc387377eea358b50f6348429447324bc700198_2_517x360.jpeg, https://discuss.pytorch.org/uploads/default/optimized/3X/e/d/edc387377eea358b50f6348429447324bc700198_2_775x540.jpeg 1.5x, https://discuss.pytorch.org/uploads/default/optimized/3X/e/d/edc387377eea358b50f6348429447324bc700198_2_1034x720.jpeg 2x" data-dominant-color="5C5C5C"><div class="meta"><svg class="fa d-icon d-icon-far-image svg-icon" aria-hidden="true"><use href="#far-image"></use></svg><span class="filename">collage</span><span class="informations">2800×1955 669 KB</span><svg class="fa d-icon d-icon-discourse-expand svg-icon" aria-hidden="true"><use href="#discourse-expand"></use></svg></div></a></div><br>
Here is the code that demonstrates the problem:</p>
<pre><code class="lang-auto">import numpy as np
import time
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader
from torch.utils.data.dataloader import default_collate
from torch.utils.data.sampler import SubsetRandomSampler
from torchvision import datasets, transforms
from torch.utils.data import Subset
from torch.optim import Adam
from torch.optim.lr_scheduler import StepLR
from torch.utils.tensorboard import SummaryWriter

WEIGHT_DECAY = 0.1
EPOCHS = 10


class BaseDataLoader(DataLoader):
    """
    Base class for all data loaders
    """
    def __init__(self, dataset, batch_size, shuffle, validation_split, num_workers, collate_fn=default_collate):
        self.validation_split = validation_split
        self.shuffle = shuffle

        self.batch_idx = 0
        self.n_samples = len(dataset)

        self.sampler, self.valid_sampler = self._split_sampler(self.validation_split)

        self.init_kwargs = {
            'dataset': dataset,
            'batch_size': batch_size,
            'shuffle': self.shuffle,
            'collate_fn': collate_fn,
            'num_workers': num_workers
        }
        super().__init__(sampler=self.sampler, **self.init_kwargs)

    def _split_sampler(self, split):
        if split == 0.0:
            return None, None

        idx_full = np.arange(self.n_samples)

        np.random.seed(0)
        np.random.shuffle(idx_full)

        if isinstance(split, int):
            assert split &gt; 0
            assert split &lt; self.n_samples, "validation set size is configured to be larger than entire dataset."
            len_valid = split
        else:
            len_valid = int(self.n_samples * split)

        valid_idx = idx_full[0:len_valid]
        train_idx = np.delete(idx_full, np.arange(0, len_valid))

        train_sampler = SubsetRandomSampler(train_idx)
        valid_sampler = SubsetRandomSampler(valid_idx)

        # turn off shuffle option which is mutually exclusive with sampler
        self.shuffle = False
        self.n_samples = len(train_idx)

        return train_sampler, valid_sampler

    def split_validation(self):
        if self.valid_sampler is None:
            return None
        else:
            return DataLoader(sampler=self.valid_sampler, **self.init_kwargs)


class ADTargetTransform(object):
    """
    Transform target labels to 0 for normal, 1 for anomaly.
    """
    def __init__(self, normal_classes):
        self.normal_classes = normal_classes

    def __call__(self, target):
        return int(target not in self.normal_classes)


class MnistADDataLoader(BaseDataLoader):
    def __init__(self, data_dir, normal_classes, batch_size, oe_frac=0.0, shuffle=True, validation_split=0.0, num_workers=1, training=True):
        # Create normal and outlier classes tuples
        self.normal_classes = tuple(normal_classes)
        self.outlier_classes = tuple([x for x in list(range(0, 10)) if x not in self.normal_classes])

        trsfm = transforms.Compose([
            transforms.ToTensor(),
            transforms.Normalize((0.1307,), (0.3081,))
        ])
        target_transform = ADTargetTransform(self.normal_classes)

        self.data_dir = data_dir
        mnist_dataset = datasets.MNIST(self.data_dir, train=training, download=True, transform=trsfm, target_transform=target_transform)

        # Get indexes of normal and outlier classes
        normal_idx = np.argwhere(np.isin(mnist_dataset.targets.cpu().data.numpy(), self.normal_classes)).flatten().tolist()
        outlier_idx = np.argwhere(np.isin(mnist_dataset.targets.cpu().data.numpy(), self.outlier_classes)).flatten().tolist()

        idx = normal_idx
        if oe_frac &gt; 0:
            # Calculate how many outlier samples will be added to dataset
            n_outliers_to_use = int(round(len(normal_idx)*oe_frac/(1-oe_frac)))
            if n_outliers_to_use &gt; len(outlier_idx):
                n_outliers_to_use = len(outlier_idx)
            idx += np.random.choice(outlier_idx, n_outliers_to_use).tolist()
            np.random.shuffle(idx)

        self.dataset = Subset(mnist_dataset, idx)
        super().__init__(self.dataset, batch_size, shuffle, validation_split, num_workers)


class MnistModel(nn.Module):
    def __init__(self, output_dim=10):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
        self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
        self.conv2_drop = nn.Dropout2d()
        self.fc1 = nn.Linear(320, 150)
        self.fc2 = nn.Linear(150, 80)
        self.fc3 = nn.Linear(80, output_dim)

    def forward(self, x):
        x = F.relu(F.max_pool2d(self.conv1(x), 2))
        x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2))
        x = x.view(-1, 320)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        out = self.fc3(x)
        return out


def loss_f(output, target):
    eps = 1e-9
    dists = torch.norm(output, p=2, dim=1) ** 2
    losses = torch.where(target == 0, dists, -torch.log(1 - torch.exp(-dists) + eps))
    return torch.mean(losses)


if __name__ == '__main__':
    device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
    data_loader = MnistADDataLoader(data_dir="data/", normal_classes=[0], batch_size=128,
                                    oe_frac=0.1, shuffle=True, validation_split=0.1, num_workers=2)
    valid_data_loader = data_loader.split_validation()
    writer = SummaryWriter('log')
    model = MnistModel(10).to(device)
    trainable_params = filter(lambda p: p.requires_grad, model.parameters())
    optimizer = Adam(trainable_params, lr=0.001, weight_decay=WEIGHT_DECAY, amsgrad=True)
    lr_scheduler = StepLR(optimizer, step_size=50, gamma=0.1)
    for epoch in range(0, EPOCHS):
        print("Epoch", epoch, ":")
        model.train()
        for batch_idx, (data, target) in enumerate(data_loader):
            data, target = data.to(device), target.to(device)
            optimizer.zero_grad()
            output = model(data)
            loss = loss_f(output, target)
            loss.backward()
            optimizer.step()
        start = time.time()
        for name, p in model.named_parameters():
            writer.add_histogram(name, p, bins='auto')
        end = time.time()
        print("\tHistograms:", end - start, "s")

</code></pre>
<p>I have killed quite some time to find the cause of my problem and a way to resolve it, however I still have no idea. I will be grateful if you can help me with that.<br>
Thank you!</p>