Repository navigation
Expand file tree
/
Copy pathtesting.py
More file actions
239 lines (195 loc) · 8.75 KB
/
Copy pathtesting.py
File metadata and controls
239 lines (195 loc) · 8.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
import json
import os
import re
import sys
import tempfile
from pathlib import Path
from typing import Callable, Optional, Union
import numpy as np
import torch
import torch.distributed as dist
def flush_l2_cache(enabled: bool = True):
"""
Flush the GPU L2 cache by writing a large zero-initialized tensor.
Arguments:
enabled: if `False`, does nothing.
"""
l2_flush_cache_size = 256e6
if enabled:
torch.empty(int(l2_flush_cache_size // 4), dtype=torch.int, device='cuda').zero_()
def bench(fn, num_warmups: int = 30, num_tests: int = 30,
post_fn: Optional[Callable] = None, flush_l2: bool = True):
"""
Benchmark a function using CUDA events.
Arguments:
fn: the function to benchmark.
num_warmups: the number of warmup iterations.
num_tests: the number of measurement iterations.
post_fn: an optional function to call after each test iteration.
flush_l2: whether to flush the L2 cache before each iteration.
Returns:
avg: the average execution time in seconds.
min: the minimum execution time in seconds.
max: the maximum execution time in seconds.
"""
torch.cuda.synchronize()
# Warmup
for _ in range(num_warmups):
fn()
# Testing
start_events = [torch.cuda.Event(enable_timing=True) for _ in range(num_tests)]
end_events = [torch.cuda.Event(enable_timing=True) for _ in range(num_tests)]
for i in range(num_tests):
flush_l2_cache(flush_l2)
start_events[i].record()
fn()
end_events[i].record()
if post_fn is not None:
post_fn()
torch.cuda.synchronize()
times = np.array([s.elapsed_time(e) / 1e3 for s, e in zip(start_events, end_events)])[1:]
return np.average(times), np.min(times), np.max(times)
class empty_suppress:
def __enter__(self):
return self
def __exit__(self, *_):
pass
class suppress_stdout_stderr:
"""
Context manager to suppress stdout and stderr output.
"""
def __enter__(self):
self.outnull_file = open(os.devnull, 'w')
self.errnull_file = open(os.devnull, 'w')
self.old_stdout_fileno_undup = sys.stdout.fileno()
self.old_stderr_fileno_undup = sys.stderr.fileno()
self.old_stdout_fileno = os.dup(sys.stdout.fileno())
self.old_stderr_fileno = os.dup(sys.stderr.fileno())
self.old_stdout = sys.stdout
self.old_stderr = sys.stderr
os.dup2(self.outnull_file.fileno(), self.old_stdout_fileno_undup)
os.dup2(self.errnull_file.fileno(), self.old_stderr_fileno_undup)
sys.stdout = self.outnull_file
sys.stderr = self.errnull_file
return self
def __exit__(self, *_):
sys.stdout = self.old_stdout
sys.stderr = self.old_stderr
os.dup2(self.old_stdout_fileno, self.old_stdout_fileno_undup)
os.dup2(self.old_stderr_fileno, self.old_stderr_fileno_undup)
os.close(self.old_stdout_fileno)
os.close(self.old_stderr_fileno)
self.outnull_file.close()
self.errnull_file.close()
def bench_kineto(fn,
kernel_names: Union[str, tuple],
num_tests: int = 30,
suppress_kineto_output: bool = False,
trace_path: Optional[str] = None,
flush_l2: bool = True,
barrier_comm_profiling: bool = False,
num_kernels_per_period: int = 1,
barrier: Optional[Callable] = None):
"""
Benchmark a function using the PyTorch profiler (kineto) to get per-kernel timing.
Arguments:
fn: the function to benchmark.
kernel_names: the CUDA kernel name(s) to profile.
num_tests: the number of test iterations.
suppress_kineto_output: whether to suppress profiler output.
trace_path: the path to save the Chrome trace (`None` to skip).
flush_l2: whether to flush the L2 cache before each iteration.
barrier_comm_profiling: whether to insert a barrier before each iteration to reduce
unbalanced CPU launch overhead.
num_kernels_per_period: the number of kernels launched per test period.
barrier: a custom barrier function to use instead of `dist.all_reduce`.
Returns:
durations: the average kernel duration(s) in seconds.
"""
assert isinstance(kernel_names, (str, tuple))
is_tuple = isinstance(kernel_names, tuple)
# Skip profiling
# Conflict with Nsight Systems, Nsight Compute and Compute Sanitizer
if int(os.environ.get('EP_USE_NVIDIA_TOOLS', 0)):
return (1, ) * len(kernel_names) if is_tuple else 1
# For some auto-tuning kernels with prints
fn()
torch.cuda.synchronize()
# Profile
suppress = suppress_stdout_stderr if suppress_kineto_output else empty_suppress
barrier_comm_profiling &= int(os.environ.get('EP_DISABLE_BARRIER_PROFILING', 0)) == 0
with suppress():
schedule = torch.profiler.schedule(wait=0, warmup=1, active=1, repeat=1)
profiler = torch.profiler.profile(activities=[torch.profiler.ProfilerActivity.CUDA], schedule=schedule, acc_events=True)
dummy = torch.ones(1, dtype=torch.float, device='cuda')
with profiler:
for i in range(2):
for _ in range(num_tests):
# Flush L2 cache
flush_l2_cache(flush_l2)
# NOTES: use a large kernel and a barrier to eliminate the unbalanced CPU launch overhead
if barrier_comm_profiling:
torch.cuda._sleep(int(2e7)) # ~10ms
# Some network may have ring-based implement, so be careful to use `all_reduce`
if barrier is None:
dist.all_reduce(dummy)
else:
barrier()
fn()
torch.cuda.synchronize()
profiler.step()
# Parse the profiling table
prof_lines = profiler.key_averages().table(sort_by='cuda_time_total', max_name_column_width=100).split('\n')
kernel_names = (kernel_names, ) if isinstance(kernel_names, str) else kernel_names
assert all([isinstance(name, str) for name in kernel_names])
for name in kernel_names:
assert sum([name in line for line in prof_lines]) <= 1, f'Errors of the kernel {name} in the profiling table: {prof_lines}'
# Save chrome traces
if trace_path is not None:
profiler.export_chrome_trace(trace_path)
# Return average kernel durations
units = {'ms': 1e3, 'us': 1e6}
kernel_durations = []
for name in kernel_names:
total_time = 0
total_num = 0
for line in prof_lines:
if name in line:
time_str = line.split()[-2]
num_str = line.split()[-1]
for unit, scale in units.items():
if unit in time_str:
total_time += float(time_str.replace(unit, '')) / scale * int(num_str)
total_num += int(num_str)
break
kernel_durations.append(total_time / total_num if total_num > 0 else 0)
# Expand the kernels by periods
if num_kernels_per_period > 1:
with tempfile.NamedTemporaryFile(suffix='.json') as tmp:
profiler.export_chrome_trace(tmp.name)
profile_data = json.loads(Path(tmp.name).read_text())
for i, kernel_name in enumerate(kernel_names):
events = [event for event in profile_data['traceEvents'] if f'::{kernel_name}' in event['name']]
events = sorted(events, key=lambda event: event['ts'])
durations = [event['dur'] / 1e6 for event in events]
assert len(durations) % num_kernels_per_period == 0
num_kernel_patterns = len(durations) // num_kernels_per_period
kernel_durations[i] = [sum(durations[j::num_kernels_per_period]) / num_kernel_patterns for j in range(num_kernels_per_period)]
# Return execution durations
return kernel_durations if is_tuple else kernel_durations[0]
def parse_num_bytes(text: str) -> int:
"""Parse a byte count, optionally with a binary suffix: `1G`, `64M`, `512k`.
"""
match = re.fullmatch(r'\s*([0-9.]+)\s*([kmgt])?(?:i?b)?\s*', text, re.IGNORECASE)
if match is None:
raise ValueError(
f'{text!r} is not a byte count; expected e.g. 1G, 64M, 512K, 1048576')
scale = {None: 1, 'k': 1 << 10, 'm': 1 << 20, 'g': 1 << 30, 't': 1 << 40}
suffix = match.group(2)
try:
value = int(float(match.group(1)) * scale[suffix.lower() if suffix else None])
except ValueError:
raise ValueError(f'{text!r} has an unparseable number')
if value <= 0:
raise ValueError(f'byte count must be positive, got {value}')
return value