|
|
|
import logging |
|
import shutil |
|
import sys |
|
import tempfile |
|
from unittest.mock import MagicMock, Mock, call, patch |
|
|
|
import numpy as np |
|
import pytest |
|
import torch |
|
import torch.nn as nn |
|
from mmcv.runner import (CheckpointHook, IterTimerHook, PaviLoggerHook, |
|
build_runner) |
|
from torch.nn.init import constant_ |
|
from torch.utils.data import DataLoader, Dataset |
|
|
|
from mmdet.core.hook import ExpMomentumEMAHook, YOLOXLrUpdaterHook |
|
from mmdet.core.hook.sync_norm_hook import SyncNormHook |
|
from mmdet.core.hook.sync_random_size_hook import SyncRandomSizeHook |
|
|
|
|
|
def _build_demo_runner_without_hook(runner_type='EpochBasedRunner', |
|
max_epochs=1, |
|
max_iters=None, |
|
multi_optimziers=False): |
|
|
|
class Model(nn.Module): |
|
|
|
def __init__(self): |
|
super().__init__() |
|
self.linear = nn.Linear(2, 1) |
|
self.conv = nn.Conv2d(3, 3, 3) |
|
|
|
def forward(self, x): |
|
return self.linear(x) |
|
|
|
def train_step(self, x, optimizer, **kwargs): |
|
return dict(loss=self(x)) |
|
|
|
def val_step(self, x, optimizer, **kwargs): |
|
return dict(loss=self(x)) |
|
|
|
model = Model() |
|
|
|
if multi_optimziers: |
|
optimizer = { |
|
'model1': |
|
torch.optim.SGD(model.linear.parameters(), lr=0.02, momentum=0.95), |
|
'model2': |
|
torch.optim.SGD(model.conv.parameters(), lr=0.01, momentum=0.9), |
|
} |
|
else: |
|
optimizer = torch.optim.SGD(model.parameters(), lr=0.02, momentum=0.95) |
|
|
|
tmp_dir = tempfile.mkdtemp() |
|
runner = build_runner( |
|
dict(type=runner_type), |
|
default_args=dict( |
|
model=model, |
|
work_dir=tmp_dir, |
|
optimizer=optimizer, |
|
logger=logging.getLogger(), |
|
max_epochs=max_epochs, |
|
max_iters=max_iters)) |
|
return runner |
|
|
|
|
|
def _build_demo_runner(runner_type='EpochBasedRunner', |
|
max_epochs=1, |
|
max_iters=None, |
|
multi_optimziers=False): |
|
log_config = dict( |
|
interval=1, hooks=[ |
|
dict(type='TextLoggerHook'), |
|
]) |
|
|
|
runner = _build_demo_runner_without_hook(runner_type, max_epochs, |
|
max_iters, multi_optimziers) |
|
|
|
runner.register_checkpoint_hook(dict(interval=1)) |
|
runner.register_logger_hooks(log_config) |
|
return runner |
|
|
|
|
|
@pytest.mark.parametrize('multi_optimziers', (True, False)) |
|
def test_yolox_lrupdater_hook(multi_optimziers): |
|
"""xdoctest -m tests/test_hooks.py test_cosine_runner_hook.""" |
|
|
|
YOLOXLrUpdaterHook(0, min_lr_ratio=0.05) |
|
|
|
sys.modules['pavi'] = MagicMock() |
|
loader = DataLoader(torch.ones((10, 2))) |
|
runner = _build_demo_runner(multi_optimziers=multi_optimziers) |
|
|
|
hook_cfg = dict( |
|
type='YOLOXLrUpdaterHook', |
|
warmup='exp', |
|
by_epoch=False, |
|
warmup_by_epoch=True, |
|
warmup_ratio=1, |
|
warmup_iters=5, |
|
num_last_epochs=15, |
|
min_lr_ratio=0.05) |
|
runner.register_hook_from_cfg(hook_cfg) |
|
runner.register_hook_from_cfg(dict(type='IterTimerHook')) |
|
runner.register_hook(IterTimerHook()) |
|
|
|
|
|
hook = PaviLoggerHook(interval=1, add_graph=False, add_last_ckpt=True) |
|
runner.register_hook(hook) |
|
runner.run([loader], [('train', 1)]) |
|
shutil.rmtree(runner.work_dir) |
|
|
|
|
|
assert hasattr(hook, 'writer') |
|
if multi_optimziers: |
|
calls = [ |
|
call( |
|
'train', { |
|
'learning_rate/model1': 8.000000000000001e-06, |
|
'learning_rate/model2': 4.000000000000001e-06, |
|
'momentum/model1': 0.95, |
|
'momentum/model2': 0.9 |
|
}, 1), |
|
call( |
|
'train', { |
|
'learning_rate/model1': 0.00039200000000000004, |
|
'learning_rate/model2': 0.00019600000000000002, |
|
'momentum/model1': 0.95, |
|
'momentum/model2': 0.9 |
|
}, 7), |
|
call( |
|
'train', { |
|
'learning_rate/model1': 0.0008000000000000001, |
|
'learning_rate/model2': 0.0004000000000000001, |
|
'momentum/model1': 0.95, |
|
'momentum/model2': 0.9 |
|
}, 10) |
|
] |
|
else: |
|
calls = [ |
|
call('train', { |
|
'learning_rate': 8.000000000000001e-06, |
|
'momentum': 0.95 |
|
}, 1), |
|
call('train', { |
|
'learning_rate': 0.00039200000000000004, |
|
'momentum': 0.95 |
|
}, 7), |
|
call('train', { |
|
'learning_rate': 0.0008000000000000001, |
|
'momentum': 0.95 |
|
}, 10) |
|
] |
|
hook.writer.add_scalars.assert_has_calls(calls, any_order=True) |
|
|
|
|
|
def test_ema_hook(): |
|
"""xdoctest -m tests/test_hooks.py test_ema_hook.""" |
|
|
|
class DemoModel(nn.Module): |
|
|
|
def __init__(self): |
|
super().__init__() |
|
self.conv = nn.Conv2d( |
|
in_channels=1, |
|
out_channels=2, |
|
kernel_size=1, |
|
padding=1, |
|
bias=True) |
|
self.bn = nn.BatchNorm2d(2) |
|
|
|
self._init_weight() |
|
|
|
def _init_weight(self): |
|
constant_(self.conv.weight, 0) |
|
constant_(self.conv.bias, 0) |
|
constant_(self.bn.weight, 0) |
|
constant_(self.bn.bias, 0) |
|
|
|
def forward(self, x): |
|
return self.bn(self.conv(x)).sum() |
|
|
|
def train_step(self, x, optimizer, **kwargs): |
|
return dict(loss=self(x)) |
|
|
|
def val_step(self, x, optimizer, **kwargs): |
|
return dict(loss=self(x)) |
|
|
|
loader = DataLoader(torch.ones((1, 1, 1, 1))) |
|
runner = _build_demo_runner() |
|
demo_model = DemoModel() |
|
runner.model = demo_model |
|
ema_hook = ExpMomentumEMAHook( |
|
momentum=0.0002, |
|
total_iter=1, |
|
skip_buffers=True, |
|
interval=2, |
|
resume_from=None) |
|
checkpointhook = CheckpointHook(interval=1, by_epoch=True) |
|
runner.register_hook(ema_hook, priority='HIGHEST') |
|
runner.register_hook(checkpointhook) |
|
runner.run([loader, loader], [('train', 1), ('val', 1)]) |
|
checkpoint = torch.load(f'{runner.work_dir}/epoch_1.pth') |
|
num_eam_params = 0 |
|
for name, value in checkpoint['state_dict'].items(): |
|
if 'ema' in name: |
|
num_eam_params += 1 |
|
value.fill_(1) |
|
assert num_eam_params == 4 |
|
torch.save(checkpoint, f'{runner.work_dir}/epoch_1.pth') |
|
|
|
work_dir = runner.work_dir |
|
resume_ema_hook = ExpMomentumEMAHook( |
|
momentum=0.5, |
|
total_iter=10, |
|
skip_buffers=True, |
|
interval=1, |
|
resume_from=f'{work_dir}/epoch_1.pth') |
|
runner = _build_demo_runner(max_epochs=2) |
|
runner.model = demo_model |
|
runner.register_hook(resume_ema_hook, priority='HIGHEST') |
|
checkpointhook = CheckpointHook(interval=1, by_epoch=True) |
|
runner.register_hook(checkpointhook) |
|
runner.run([loader, loader], [('train', 1), ('val', 1)]) |
|
checkpoint = torch.load(f'{runner.work_dir}/epoch_2.pth') |
|
num_eam_params = 0 |
|
desired_output = [0.9094, 0.9094] |
|
for name, value in checkpoint['state_dict'].items(): |
|
if 'ema' in name: |
|
num_eam_params += 1 |
|
assert value.sum() == 2 |
|
else: |
|
if ('weight' in name) or ('bias' in name): |
|
np.allclose(value.data.cpu().numpy().reshape(-1), |
|
desired_output, 1e-4) |
|
assert num_eam_params == 4 |
|
shutil.rmtree(runner.work_dir) |
|
shutil.rmtree(work_dir) |
|
|
|
|
|
def test_sync_norm_hook(): |
|
|
|
SyncNormHook() |
|
|
|
loader = DataLoader(torch.ones((5, 2))) |
|
runner = _build_demo_runner() |
|
runner.register_hook_from_cfg(dict(type='SyncNormHook')) |
|
runner.run([loader, loader], [('train', 1), ('val', 1)]) |
|
shutil.rmtree(runner.work_dir) |
|
|
|
|
|
def test_sync_random_size_hook(): |
|
|
|
SyncRandomSizeHook() |
|
|
|
class DemoDataset(Dataset): |
|
|
|
def __getitem__(self, item): |
|
return torch.ones(2) |
|
|
|
def __len__(self): |
|
return 5 |
|
|
|
def update_dynamic_scale(self, dynamic_scale): |
|
pass |
|
|
|
loader = DataLoader(DemoDataset()) |
|
runner = _build_demo_runner() |
|
runner.register_hook_from_cfg( |
|
dict(type='SyncRandomSizeHook', device='cpu')) |
|
runner.run([loader, loader], [('train', 1), ('val', 1)]) |
|
shutil.rmtree(runner.work_dir) |
|
|
|
if torch.cuda.is_available(): |
|
runner = _build_demo_runner() |
|
runner.register_hook_from_cfg( |
|
dict(type='SyncRandomSizeHook', device='cuda')) |
|
runner.run([loader, loader], [('train', 1), ('val', 1)]) |
|
shutil.rmtree(runner.work_dir) |
|
|
|
|
|
@pytest.mark.parametrize('set_loss', [ |
|
dict(set_loss_nan=False, set_loss_inf=False), |
|
dict(set_loss_nan=True, set_loss_inf=False), |
|
dict(set_loss_nan=False, set_loss_inf=True) |
|
]) |
|
def test_check_invalid_loss_hook(set_loss): |
|
|
|
|
|
class DemoModel(nn.Module): |
|
|
|
def __init__(self, set_loss_nan=False, set_loss_inf=False): |
|
super().__init__() |
|
self.set_loss_nan = set_loss_nan |
|
self.set_loss_inf = set_loss_inf |
|
self.linear = nn.Linear(2, 1) |
|
|
|
def forward(self, x): |
|
return self.linear(x) |
|
|
|
def train_step(self, x, optimizer, **kwargs): |
|
if self.set_loss_nan: |
|
return dict(loss=torch.tensor(float('nan'))) |
|
elif self.set_loss_inf: |
|
return dict(loss=torch.tensor(float('inf'))) |
|
else: |
|
return dict(loss=self(x)) |
|
|
|
loader = DataLoader(torch.ones((5, 2))) |
|
runner = _build_demo_runner() |
|
|
|
demo_model = DemoModel(**set_loss) |
|
runner.model = demo_model |
|
runner.register_hook_from_cfg( |
|
dict(type='CheckInvalidLossHook', interval=1)) |
|
if not set_loss['set_loss_nan'] \ |
|
and not set_loss['set_loss_inf']: |
|
|
|
runner.run([loader], [('train', 1)]) |
|
else: |
|
|
|
with pytest.raises(AssertionError): |
|
runner.run([loader], [('train', 1)]) |
|
shutil.rmtree(runner.work_dir) |
|
|
|
|
|
def test_set_epoch_info_hook(): |
|
"""Test SetEpochInfoHook.""" |
|
|
|
class DemoModel(nn.Module): |
|
|
|
def __init__(self): |
|
super().__init__() |
|
self.epoch = 0 |
|
self.linear = nn.Linear(2, 1) |
|
|
|
def forward(self, x): |
|
return self.linear(x) |
|
|
|
def train_step(self, x, optimizer, **kwargs): |
|
return dict(loss=self(x)) |
|
|
|
def set_epoch(self, epoch): |
|
self.epoch = epoch |
|
|
|
loader = DataLoader(torch.ones((5, 2))) |
|
runner = _build_demo_runner(max_epochs=3) |
|
|
|
demo_model = DemoModel() |
|
runner.model = demo_model |
|
runner.register_hook_from_cfg(dict(type='SetEpochInfoHook')) |
|
runner.run([loader], [('train', 1)]) |
|
assert demo_model.epoch == 2 |
|
|
|
|
|
def test_memory_profiler_hook(): |
|
from collections import namedtuple |
|
|
|
|
|
with pytest.raises(ImportError): |
|
from mmdet.core.hook import MemoryProfilerHook |
|
MemoryProfilerHook(1) |
|
|
|
|
|
sys.modules['psutil'] = MagicMock() |
|
with pytest.raises(ImportError): |
|
from mmdet.core.hook import MemoryProfilerHook |
|
MemoryProfilerHook(1) |
|
|
|
sys.modules['memory_profiler'] = MagicMock() |
|
|
|
def _mock_virtual_memory(): |
|
virtual_memory_type = namedtuple( |
|
'virtual_memory', ['total', 'available', 'percent', 'used']) |
|
return virtual_memory_type( |
|
total=270109085696, |
|
available=250416816128, |
|
percent=7.3, |
|
used=17840881664) |
|
|
|
def _mock_swap_memory(): |
|
swap_memory_type = namedtuple('swap_memory', [ |
|
'total', |
|
'used', |
|
'percent', |
|
]) |
|
return swap_memory_type(total=8589930496, used=0, percent=0.0) |
|
|
|
def _mock_memory_usage(): |
|
return [40.22265625] |
|
|
|
mock_virtual_memory = Mock(return_value=_mock_virtual_memory()) |
|
mock_swap_memory = Mock(return_value=_mock_swap_memory()) |
|
mock_memory_usage = Mock(return_value=_mock_memory_usage()) |
|
|
|
@patch('psutil.swap_memory', mock_swap_memory) |
|
@patch('psutil.virtual_memory', mock_virtual_memory) |
|
@patch('memory_profiler.memory_usage', mock_memory_usage) |
|
def _test_memory_profiler_hook(): |
|
from mmdet.core.hook import MemoryProfilerHook |
|
hook = MemoryProfilerHook(1) |
|
runner = _build_demo_runner() |
|
|
|
assert not mock_memory_usage.called |
|
assert not mock_swap_memory.called |
|
assert not mock_memory_usage.called |
|
|
|
hook.after_iter(runner) |
|
|
|
assert mock_memory_usage.called |
|
assert mock_swap_memory.called |
|
assert mock_memory_usage.called |
|
|
|
_test_memory_profiler_hook() |
|
|