Skip to content

Commit b7948e7

Browse files
navsudfacebook-github-bot
authored andcommitted
Update post-processing logic (#541)
Summary: Pull Request resolved: #541 Currently the post-processing needs to know the threshold and window-size to find the start/end of the benchmark window. This new post-processing logic removes the need for those two parameters. - removed energy, which is not used at all. - changed latency to ms (from us) to be consistent with the other metrics (mW) - added baseline_power output, to see how it changes over time. Reviewed By: ayushidalmia Differential Revision: D54669432 fbshipit-source-id: d143ebea841756e1a68928b3d2e44afda97d4cf5
1 parent 8344c02 commit b7948e7

File tree

3 files changed

+135
-290
lines changed

3 files changed

+135
-290
lines changed

benchmarking/frameworks/framework_base.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -314,25 +314,13 @@ def runBenchmark(self, info, benchmark, platform):
314314
voltage = (
315315
float(monsoon_args["voltage"]) if "voltage" in monsoon_args else 4.0
316316
)
317-
threshold = float(
318-
monsoon_args["threshold"] if "threshold" in monsoon_args else 300
319-
)
320-
window_size_in_ms = float(
321-
monsoon_args["window_size"]
322-
if "window_size" in monsoon_args
323-
else 1000
324-
)
325-
# each sample is 200us
326-
window_size = int(window_size_in_ms / 0.2)
327317
output = collectPowerData(
328318
platform.platform_hash,
329319
collection_time,
330320
voltage,
331321
test["iter"],
332322
method=test["method"] if "method" in test else "monsoon",
333323
monsoon_map=self.args.monsoon_map,
334-
threshold=threshold,
335-
window_size=window_size,
336324
)
337325
platform.waitForDevice(20)
338326
# kill the process if exists

benchmarking/utils/monsoon_power.py

Lines changed: 34 additions & 278 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import Monsoon.sampleEngine as sampleEngine
2424
from bridge.file_storage.upload_files.file_uploader import FileUploader
2525
from utils.custom_logger import getLogger
26+
from utils.power_utils import post_process_power_data
2627

2728

2829
def collectPowerData(
@@ -32,8 +33,6 @@ def collectPowerData(
3233
num_iters,
3334
method="monsoon",
3435
monsoon_map=None,
35-
threshold=300,
36-
window_size=1000,
3736
):
3837
has_usb = method == "monsoon_with_usb"
3938
Mon = HVPM.Monsoon()
@@ -105,79 +104,58 @@ def collectPowerData(
105104
repeat = repeat + 1
106105
sleep(2)
107106
if repeat >= 5:
108-
getLogger().info("Failed to close device")
109-
return {}
107+
raise Exception("Failed to close device")
110108

111109
power_data, url = _extract_samples(samples, has_usb)
112110

113-
getLogger().info(
114-
"Calculating the benchmark data range from "
115-
"{} data points".format(len(power_data))
116-
)
117-
max_range, max_low_range = _calculatePowerDataRange(
118-
power_data, threshold, window_size
119-
)
120-
if max_range is None or max_low_range is None:
121-
getLogger().error("Metric collection failed")
122-
return {}
123-
124-
getLogger().info(
125-
"Collecting baseline data from "
126-
"{} to {}".format(max_low_range["start"], max_low_range["end"])
127-
)
128-
getLogger().info(
129-
"Collecting data from " "{} to {}".format(max_range["start"], max_range["end"])
130-
)
131-
getLogger().info(
132-
"Benchmark time: "
133-
"{} - {} s".format(
134-
power_data[max_range["start"]]["time"], power_data[max_range["end"]]["time"]
135-
)
136-
)
137-
data = _retrievePowerData(power_data, max_range, max_low_range, num_iters)
111+
data = post_process_power_data(power_data, sample_rate=5000, num_iters=num_iters)
138112
data["power_trace"] = url
113+
139114
return data
140115

141116

142117
def _extract_samples(samples, has_usb):
143-
power_data = []
118+
power_data = {
119+
"time": [],
120+
"current": [],
121+
"voltage": [],
122+
"usb_current": [],
123+
"usb_voltage": [],
124+
"total_power": [],
125+
}
144126

145-
prev_time_stamp = -1
146127
for i in range(len(samples[sampleEngine.channels.timeStamp])):
147128
time_stamp = samples[sampleEngine.channels.timeStamp][i]
148129
current = samples[sampleEngine.channels.MainCurrent][i]
149130
voltage = samples[sampleEngine.channels.MainVoltage][i]
150131
if has_usb:
151132
usb_current = samples[sampleEngine.channels.USBCurrent][i]
152133
usb_voltage = samples[sampleEngine.channels.USBVoltage][i]
153-
# there is a bug that two consecutive time stamps may be identical
154-
# patch it by evenly divide the timestamps
155-
if i >= 2 and prev_time_stamp == time_stamp:
156-
power_data[-1]["time"] = (power_data[-2]["time"] + time_stamp) / 2
157-
prev_time_stamp = time_stamp
158-
data = {
159-
"time": time_stamp,
160-
"current": current,
161-
"voltage": voltage,
162-
"usb_current": 0,
163-
"usb_voltage": 0,
164-
}
134+
power_data["time"].append(time_stamp)
135+
power_data["current"].append(current)
136+
power_data["voltage"].append(voltage)
165137
if has_usb:
166-
data["usb_current"] = usb_current
167-
data["usb_voltage"] = usb_voltage
168-
169-
power_data.append(data)
138+
power_data["usb_current"].append(usb_current)
139+
power_data["usb_voltage"].append(usb_voltage)
140+
power_data["total_power"].append(
141+
(current * voltage) + (usb_current * usb_voltage)
142+
)
143+
else:
144+
power_data["usb_current"].append(0)
145+
power_data["usb_voltage"].append(0)
146+
power_data["total_power"].append(current * voltage)
170147

171148
with tempfile.NamedTemporaryFile(
172149
mode="w", delete=False, prefix="power_data_", suffix=".csv"
173150
) as f:
174151
filename = f.name
175152
getLogger().info("Writing power data to file: {}".format(f.name))
176-
f.write("time, current, voltage, usb_current, usb_voltage\n")
177-
for i in range(len(power_data)):
178-
entry = power_data[i]
153+
f.write("time, current, voltage, usb_current, usb_voltage, total_power\n")
154+
for i in range(len(power_data["time"])):
179155
f.write(
180-
f"{entry['time']}, {entry['current']}, {entry['voltage']}, {entry['usb_current']}, {entry['usb_voltage']}\n"
156+
f"{power_data['time'][i]:.4f}, {power_data['current'][i]:.4f}, "
157+
f"{power_data['voltage'][i]:.4f}, {power_data['usb_current'][i]:.4f}, "
158+
f"{power_data['usb_voltage'][i]:.4f}, {power_data['total_power'][i]:.4f}\n"
181159
)
182160

183161
getLogger().info(f"Uploading power file {filename}")
@@ -188,238 +166,16 @@ def _extract_samples(samples, has_usb):
188166
return power_data, url
189167

190168

191-
def _get_sum_current(power_data, start, end, window_size):
192-
# Get the total current in the window
193-
sum = 0
194-
for i in range(start, min(start + window_size, end)):
195-
sum += power_data[i]["current"]
196-
sum += power_data[i]["usb_current"]
197-
return i, sum
198-
199-
200-
def _find_first_window_below_threshold(
201-
power_data, initial_sum, start, end, window_size, threshold
202-
):
203-
return _find_first_window(
204-
power_data, initial_sum, start, end, window_size, threshold, False
205-
)
206-
207-
208-
def _find_first_window_above_threshold(
209-
power_data, initial_sum, start, end, window_size, threshold
210-
):
211-
return _find_first_window(
212-
power_data, initial_sum, start, end, window_size, threshold, True
213-
)
214-
215-
216-
def _find_first_window(
217-
power_data, initial_sum, start, end, window_size, threshold, above=True
218-
):
219-
assert start >= window_size - 1
220-
i = start
221-
sum = initial_sum
222-
while i < end - 1 and (
223-
(sum / window_size) < threshold if above else (sum / window_size) > threshold
224-
):
225-
# moving average advance one step
226-
i = i + 1
227-
sum = (
228-
sum
229-
- power_data[i - window_size]["current"]
230-
- power_data[i - window_size]["usb_current"]
231-
+ power_data[i]["current"]
232-
+ power_data[i]["usb_current"]
233-
)
234-
return i, sum
235-
236-
237-
def _calculateOnePowerDataRange(
238-
power_data, num, i, sum, threshold=300, window_size=1000
239-
):
240-
# first find the average current is less than the threshold
241-
i, sum = _find_first_window_below_threshold(
242-
power_data, sum, i, num, window_size, threshold
243-
)
244-
245-
# find the first window whose average current is above the threshold
246-
i, sum = _find_first_window_above_threshold(
247-
power_data, sum, i, num, window_size, threshold
248-
)
249-
250-
window_i = i
251-
252-
# find the last entry below threshold
253-
while (
254-
i > 0 and (power_data[i]["current"] + power_data[i]["usb_current"]) > threshold
255-
):
256-
i = i - 1
257-
# find the min of the constant decreasing current
258-
while i > 0 and (
259-
power_data[i - 1]["current"] + power_data[i - 1]["usb_current"]
260-
) < (power_data[i]["current"] + power_data[i]["usb_current"]):
261-
i = i - 1
262-
263-
# have found a possible start of the benchmark
264-
start = i
265-
266-
# find the first window whose current is below threshold again
267-
i, sum = _find_first_window_below_threshold(
268-
power_data, sum, window_i, num, window_size, threshold
269-
)
270-
ii = max(0, i - window_size)
271-
272-
# get the first entry below threshold
273-
while (
274-
ii < num
275-
and (power_data[ii]["current"] + power_data[ii]["usb_current"]) > threshold
276-
):
277-
ii = ii + 1
278-
# get the min of the constant decreasing current
279-
while ii < num - 1 and (
280-
power_data[ii]["current"] + power_data[ii]["usb_current"]
281-
) > (power_data[ii + 1]["current"] + power_data[ii + 1]["usb_current"]):
282-
ii = ii + 1
283-
284-
# found a possible end of the benchmark
285-
end = ii - 1
286-
287-
return start, end, i, sum
288-
289-
290-
# This only works in one specific scenario:
291-
# In the beginning, the current is low and below threshold
292-
# Then there is a sudden jump in current and the current keeps high
293-
# After the benchmark, the current restores back to below threshold for some time
294-
# All other scenarios are not caught
295-
def _calculatePowerDataRange(power_data, threshold=300, window_size=1000):
296-
num = len(power_data)
297-
if num <= window_size:
298-
getLogger().error(
299-
f"Collected {num} samples from monsoon, which is less than the window size of {window_size}"
300-
)
301-
return None, None
302-
# first get the sum of the window size values
303-
i, sum = _get_sum_current(power_data, 0, num, window_size)
304-
305-
ranges = []
306-
while i < num - 1:
307-
start, end, i, sum = _calculateOnePowerDataRange(
308-
power_data, num, i, sum, threshold, window_size
309-
)
310-
if (start < num) and (end <= num) and (start < end):
311-
ranges.append({"start": start, "end": end})
312-
313-
if len(ranges) == 0:
314-
getLogger().error(
315-
"Cannot collect any useful metric from the monsoon data. Please examine the benchmark setting."
316-
)
317-
return None, None
318-
319-
# get the max range of all collected ranges
320-
max_range = ranges[0]
321-
r_start = 0
322-
for r in ranges:
323-
assert r["end"] >= r["start"]
324-
assert r["start"] >= r_start
325-
r_start = r["end"]
326-
if r["end"] - r["start"] > max_range["end"] - max_range["start"]:
327-
max_range = r
328-
329-
# get the range below the threshold
330-
low_ranges = [{"start": 0, "end": -1}]
331-
for r in ranges:
332-
low_ranges[-1]["end"] = max(r["start"] - 1, low_ranges[-1]["start"])
333-
low_ranges.append({"start": r["end"] + 1, "end": -1})
334-
low_ranges[-1]["end"] = num - 1
335-
336-
# get the max range that is below the threshold
337-
max_low_range = low_ranges[0]
338-
for r in low_ranges:
339-
if r["end"] - r["start"] > max_low_range["end"] - max_low_range["start"]:
340-
max_low_range = r
341-
getLogger().info(ranges)
342-
getLogger().info(low_ranges)
343-
# the test needs to be designed in a way that more than half of the collected
344-
# data is executing the model.
345-
"""
346-
assert (
347-
max_range["end"] - max_range["start"] >= num / 2
348-
), f"Test needs to be designed that over half of the collected data is model execution. "
349-
"""
350-
return max_range, max_low_range
351-
352-
353-
def _retrievePowerData(power_data, high_range, low_range, num_iters):
354-
data = {}
355-
if high_range["start"] < 0 or high_range["end"] < 0:
356-
return data
357-
358-
# get base current. It is just an approximation
359-
360-
total_current = 0
361-
total_usb_current = 0
362-
count = 0
363-
for i in range(low_range["start"], low_range["end"]):
364-
total_current += power_data[i]["current"]
365-
total_usb_current += power_data[i]["usb_current"]
366-
count += 1
367-
base_current = total_current / count if count > 0 else 0
368-
base_usb_current = total_usb_current / count if count > 0 else 0
369-
370-
energy = 0
371-
prev_time = power_data[max(0, high_range["start"] - 1)]["time"]
372-
for i in range(high_range["start"], high_range["end"]):
373-
entry = power_data[i]
374-
curr_time = entry["time"]
375-
energy += (
376-
entry["voltage"] * (entry["current"] - base_current)
377-
+ entry["usb_voltage"] * (entry["usb_current"] - base_usb_current)
378-
) * (curr_time - prev_time)
379-
prev_time = curr_time
380-
total_time = (
381-
power_data[high_range["end"]]["time"] - power_data[high_range["start"]]["time"]
382-
)
383-
power = energy / total_time
384-
energy_per_inference = energy / num_iters
385-
latency = total_time * 1000 * 1000 / num_iters
386-
data["energy"] = _composeStructuredData(energy_per_inference, "energy", "mJ")
387-
data["power"] = _composeStructuredData(power, "power", "mW")
388-
data["latency"] = _composeStructuredData(latency, "latency", "uS")
389-
390-
getLogger().info(f"Number of iterations: {num_iters}")
391-
getLogger().info("Base current: {} mA".format(base_current))
392-
getLogger().info("Energy per inference: {} mJ".format(energy_per_inference))
393-
getLogger().info("Power: {} mW".format(power))
394-
getLogger().info("Latency per inference: {} uS".format(latency))
395-
return data
396-
397-
398-
def _composeStructuredData(data, metric, unit):
399-
return {
400-
"values": [data],
401-
"type": "NET",
402-
"metric": metric,
403-
"unit": unit,
404-
"summary": {
405-
"p0": data,
406-
"p10": data,
407-
"p50": data,
408-
"p90": data,
409-
"p100": data,
410-
"mean": data,
411-
"stdev": 0,
412-
"MAD": 0,
413-
},
414-
}
415-
416-
417169
def _getMonsoonSerialno(device_hash, monsoon_map=None, Monsoon=None):
418170
if monsoon_map:
419171
mapping = json.loads(monsoon_map)
420172
serialno = mapping.get(device_hash, None)
421173
if not serialno and len(Monsoon.enumerateDevices()) > 1:
422174
getLogger().info(
423-
f"Device {device_hash} is not associated with a specific power monitor, but there are {len(Monsoon.enumerateDevices())} Monsoon monitors connected ({Monsoon.enumerateDevices()}). Please add --monsoon_map to specify the mapping between device hash and Monsoon serial number"
175+
f"Device {device_hash} is not associated with a specific power monitor, "
176+
f"but there are {len(Monsoon.enumerateDevices())} Monsoon monitors "
177+
f"connected: {Monsoon.enumerateDevices()}. Please add --monsoon_map to "
178+
f"specify the mapping between device hash and Monsoon serial number"
424179
)
180+
425181
return serialno

0 commit comments

Comments
 (0)