|
3 | 3 |
|
4 | 4 | # DeepSpeed Team |
5 | 5 |
|
| 6 | +from types import SimpleNamespace |
| 7 | +from unittest.mock import Mock |
| 8 | + |
| 9 | +import pytest |
| 10 | +import torch |
| 11 | + |
6 | 12 | from deepspeed.utils.comms_logging import CommsLogger |
7 | 13 |
|
8 | 14 |
|
@@ -49,3 +55,146 @@ def test_trim_mean_does_not_mutate_its_argument(): |
49 | 55 | data = [3.0, 1.0, 2.0] |
50 | 56 | assert trim_mean(data, 0.1) == 2.0 |
51 | 57 | assert data == [3.0, 1.0, 2.0] |
| 58 | + |
| 59 | + |
| 60 | +def test_timed_op_falls_back_to_the_op_name_when_log_name_is_missing(monkeypatch): |
| 61 | + # timed_op looks up func_args['log_name'], so an op whose signature does not |
| 62 | + # declare log_name used to raise KeyError as soon as profiling was turned on. |
| 63 | + # Such an op must still be logged, under its own name. |
| 64 | + from deepspeed.comm import comm |
| 65 | + |
| 66 | + monkeypatch.setattr(comm, 'comms_logger', CommsLogger()) |
| 67 | + monkeypatch.setattr( |
| 68 | + comm, 'cdb', SimpleNamespace(using_mpi=False, is_initialized=lambda: True, |
| 69 | + get_world_size=lambda group=None: 1)) |
| 70 | + monkeypatch.setattr(comm, 'get_accelerator', lambda: SimpleNamespace(synchronize=lambda: None)) |
| 71 | + |
| 72 | + @comm.timed_op |
| 73 | + def barrier(): |
| 74 | + return 'done' |
| 75 | + |
| 76 | + comm.comms_logger.enabled = True |
| 77 | + comm.comms_logger.start_profiling_comms() |
| 78 | + |
| 79 | + assert barrier() == 'done' |
| 80 | + assert 'barrier' in comm.comms_logger.comms_dict |
| 81 | + |
| 82 | + |
| 83 | +def test_timed_op_profiles_default_log_name_with_prof_ops(monkeypatch): |
| 84 | + from deepspeed.comm import comm |
| 85 | + |
| 86 | + monkeypatch.setattr(comm, 'comms_logger', CommsLogger()) |
| 87 | + monkeypatch.setattr( |
| 88 | + comm, 'cdb', SimpleNamespace(using_mpi=False, is_initialized=lambda: True, |
| 89 | + get_world_size=lambda group=None: 1)) |
| 90 | + monkeypatch.setattr(comm, 'get_accelerator', lambda: SimpleNamespace(synchronize=lambda: None)) |
| 91 | + |
| 92 | + @comm.timed_op |
| 93 | + def barrier(log_name='barrier'): |
| 94 | + return 'done' |
| 95 | + |
| 96 | + comm.comms_logger.enabled = True |
| 97 | + comm.comms_logger.prof_ops = ['barrier'] |
| 98 | + |
| 99 | + assert barrier() == 'done' |
| 100 | + assert 'barrier' in comm.comms_logger.comms_dict |
| 101 | + |
| 102 | + |
| 103 | +@pytest.mark.parametrize('op_name', ['broadcast_object_list', 'all_to_all']) |
| 104 | +@pytest.mark.parametrize('positional', [False, True]) |
| 105 | +@pytest.mark.parametrize('debug', [False, True]) |
| 106 | +@pytest.mark.parametrize('profile_mode', ['prof_all', 'prof', 'prof_ops_default', 'prof_ops_custom', 'unselected']) |
| 107 | +def test_collective_profiling(monkeypatch, op_name, positional, debug, profile_mode): |
| 108 | + from deepspeed.comm import comm |
| 109 | + |
| 110 | + backend_op = Mock(return_value='done') |
| 111 | + backend = SimpleNamespace(using_mpi=False, is_initialized=lambda: True, get_world_size=lambda group=None: 2) |
| 112 | + setattr(backend, op_name, backend_op) |
| 113 | + monkeypatch.setattr(comm, 'cdb', backend) |
| 114 | + monkeypatch.setattr(comm, 'get_accelerator', lambda: SimpleNamespace(synchronize=lambda: None)) |
| 115 | + op_timer = Mock() |
| 116 | + op_timer.elapsed.return_value = 1.0 |
| 117 | + timers = Mock(return_value=op_timer) |
| 118 | + monkeypatch.setattr(comm, 'timers', timers) |
| 119 | + |
| 120 | + monkeypatch.setattr(comm, 'comms_logger', CommsLogger()) |
| 121 | + comm.comms_logger.enabled = True |
| 122 | + comm.comms_logger.debug = debug |
| 123 | + comm.comms_logger.prof_all = profile_mode == 'prof_all' |
| 124 | + log_name = 'custom_collective' if profile_mode in ('prof', 'prof_ops_custom') else op_name |
| 125 | + comm.comms_logger.prof_ops = [log_name] if profile_mode.startswith('prof_ops') else [] |
| 126 | + |
| 127 | + if op_name == 'broadcast_object_list': |
| 128 | + object_list = [{'value': 1}] |
| 129 | + args = (object_list, 0, None, None) |
| 130 | + expected_size = 0 |
| 131 | + else: |
| 132 | + input_list = [torch.ones(4), torch.ones(4)] |
| 133 | + output_list = [torch.empty_like(tensor) for tensor in input_list] |
| 134 | + args = (output_list, input_list, None, False) |
| 135 | + expected_size = sum(tensor.numel() * tensor.element_size() for tensor in input_list) |
| 136 | + |
| 137 | + kwargs = {} |
| 138 | + if profile_mode in ('prof', 'prof_ops_custom'): |
| 139 | + prof = profile_mode == 'prof' |
| 140 | + if positional: |
| 141 | + args += (prof, log_name) |
| 142 | + else: |
| 143 | + kwargs = {'prof': prof, 'log_name': log_name} |
| 144 | + |
| 145 | + assert getattr(comm, op_name)(*args, **kwargs) == 'done' |
| 146 | + if op_name == 'broadcast_object_list': |
| 147 | + backend_op.assert_called_once_with(object_list=object_list, src=0, group=None, device=None) |
| 148 | + else: |
| 149 | + backend_op.assert_called_once_with(output_list, input_list, group=None, async_op=False) |
| 150 | + |
| 151 | + if profile_mode == 'unselected': |
| 152 | + assert comm.comms_logger.comms_dict == {} |
| 153 | + timers.assert_not_called() |
| 154 | + else: |
| 155 | + record_name, = comm.comms_logger.comms_dict |
| 156 | + if debug: |
| 157 | + assert record_name.startswith(log_name + ' | [Caller Func: ') |
| 158 | + else: |
| 159 | + assert record_name == log_name |
| 160 | + record = comm.comms_logger.comms_dict[record_name][expected_size] |
| 161 | + assert record[0] == 1 |
| 162 | + assert record[1] == [1.0] |
| 163 | + op_timer.start.assert_called_once_with() |
| 164 | + op_timer.stop.assert_called_once_with() |
| 165 | + op_timer.elapsed.assert_called_once_with(reset=False) |
| 166 | + assert all(call.args == (record_name, ) for call in timers.call_args_list) |
| 167 | + |
| 168 | + |
| 169 | +def test_timed_op_disabled_does_not_access_profiling_state(monkeypatch): |
| 170 | + from deepspeed.comm import comm |
| 171 | + |
| 172 | + @comm.timed_op |
| 173 | + def barrier(prof=False, log_name='barrier'): |
| 174 | + return 'done' |
| 175 | + |
| 176 | + # No other logger attributes or backend should be needed on the disabled path. |
| 177 | + monkeypatch.setattr(comm, 'comms_logger', SimpleNamespace(enabled=False)) |
| 178 | + monkeypatch.setattr(comm, 'cdb', None) |
| 179 | + timers = Mock() |
| 180 | + monkeypatch.setattr(comm, 'timers', timers) |
| 181 | + |
| 182 | + assert barrier(True, 'custom_barrier') == 'done' |
| 183 | + timers.assert_not_called() |
| 184 | + |
| 185 | + |
| 186 | +@pytest.mark.parametrize('world_size', [1, 2, 4]) |
| 187 | +@pytest.mark.parametrize('comm_op', ['broadcast_object_list', 'all_to_all']) |
| 188 | +def test_calc_bw_log_supports_object_and_list_collectives(monkeypatch, comm_op, world_size): |
| 189 | + import deepspeed.comm as dist |
| 190 | + from deepspeed.utils.comms_logging import calc_bw_log |
| 191 | + |
| 192 | + monkeypatch.setattr(dist, 'get_world_size', lambda group=None: world_size) |
| 193 | + |
| 194 | + tput, busbw = calc_bw_log(comm_op, 1024, 2.0) |
| 195 | + expected_tput = 1024 / 2.0 * 8 / 1e6 |
| 196 | + expected_busbw = expected_tput |
| 197 | + if comm_op == 'all_to_all': |
| 198 | + expected_busbw *= (world_size - 1) / world_size |
| 199 | + assert tput == pytest.approx(expected_tput) |
| 200 | + assert busbw == pytest.approx(expected_busbw) |
0 commit comments