You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

step_trace_parser.py 15 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. # Copyright 2020 Huawei Technologies Co., Ltd
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. # ============================================================================
  15. """The parser for step trace data."""
  16. import csv
  17. import json
  18. import os
  19. import stat
  20. import struct
  21. from collections import namedtuple
  22. from decimal import Decimal
  23. from mindspore.profiler.common.exceptions.exceptions import ProfilerPathErrorException, \
  24. JobIdMismatchException, ProfilerIOException
  25. from mindspore import log
  26. from mindspore.profiler.common.util import get_summary_for_step_trace
  27. from mindspore.profiler.common.validator.validate_path import \
  28. validate_and_normalize_path
  29. StepTraceStruct = namedtuple(
  30. 'TrainingTraceStruct', ['tag_id', 'task_id', 'stream_id', 'sys_count']
  31. )
  32. class StepTraceParser:
  33. """
  34. The parser for step trace data.
  35. Args:
  36. input_dir (str): The directory that contains original step trace data.
  37. output_file_path (str): The output file path.
  38. job_id (int): The job id used to define the start of new step. Default: 0.
  39. skip_first_step (bool): Whether skip the first step or not.
  40. """
  41. _event_size = 20
  42. _fp_tag = 1
  43. _bp_tag = 2
  44. _end_tag = 255
  45. def __init__(self, input_dir, output_file_path, job_id=0, skip_first_step=False):
  46. self._input_dir = input_dir
  47. self._output_path = output_file_path
  48. self._job_id = job_id
  49. self._skip_first_step = skip_first_step
  50. self._result = []
  51. self._header = []
  52. self._step_num = 0
  53. self._tag_map = {}
  54. @property
  55. def output_file(self):
  56. """The property of step trace header."""
  57. file_name = self._output_path.rsplit('/', 2)
  58. return file_name[-1] if len(file_name) == 3 else ''
  59. def show(self):
  60. """The property of step trace info."""
  61. summary_info = {}
  62. if self._result:
  63. summary_info = get_summary_for_step_trace(self._result[-1], self._header)
  64. summary_info['total_steps'] = len(self._result) - 1
  65. print('\nStep trace summary info (unit: syscnt):')
  66. print(summary_info)
  67. print('\nThe step trace parse result saves under ${summary_dir}/profiler/%s'
  68. % self.output_file)
  69. def parse_and_save(self):
  70. """Parse step trace files and save the result."""
  71. try:
  72. source_files = self._get_step_trace_files()
  73. self._parse(source_files)
  74. self._save()
  75. except IOError as err:
  76. log.warning(err)
  77. raise ProfilerIOException()
  78. else:
  79. log.info("Finish to save intermediate result for step trace file.")
  80. def record_point_info(self, point_info, output_path):
  81. """
  82. Record point info into json.
  83. Args:
  84. point_info (dict): The point info about tag id and relative op name.
  85. output_path (str): The output path for saving point info.
  86. Returns:
  87. dict, parsed point info.
  88. """
  89. points = {
  90. 'fp_start': point_info.get(self._fp_tag, ''),
  91. 'bp_end': point_info.get(self._bp_tag, '')
  92. }
  93. try:
  94. with open(output_path, 'w') as json_file:
  95. json.dump(points, json_file)
  96. os.chmod(output_path, stat.S_IREAD)
  97. except (IOError, OSError) as err:
  98. log.warning('Failed to save point info. %s', err)
  99. raise ProfilerIOException
  100. return points
  101. def update_tag_op_type_map(self, point_info):
  102. """
  103. update the map from tag id to op type.
  104. Args:
  105. point_info (dict): The point info about tag id and relative op name.
  106. """
  107. tag_map = {}
  108. for tag, op_name in point_info.items():
  109. op_type = self._get_op_type(tag, op_name)
  110. tag_map[tag] = op_type
  111. log.info("Get tag types for step trace analysis: %s", tag_map)
  112. self._tag_map = tag_map
  113. def _get_op_type(self, tag, name):
  114. """
  115. Get op type from tag and name.
  116. Args:
  117. tag (int): The tag id.
  118. name (str): The op name.
  119. Returns:
  120. str, the op type.
  121. """
  122. tag_map = {self._fp_tag: 'fp', self._bp_tag: 'bp', self._end_tag: 'end'}
  123. # get solid tag type
  124. op_type = tag_map.get(tag, '')
  125. if op_type:
  126. return op_type
  127. # check if the tag is step tag.
  128. if tag > self._end_tag or tag == 0:
  129. return 'start'
  130. # analyze the reduce tag
  131. op_type = name.rsplit('/', 1)[-1].split('-')[0]
  132. if not op_type:
  133. log.warning("Unexpected op name:%s", name)
  134. return op_type
  135. def _get_step_trace_files(self):
  136. """Get step trace files."""
  137. # step trace files may under $profiler_dir or $profiler_dir/data
  138. profiler_dir = self._input_dir
  139. step_trace_files = self._search_file(profiler_dir)
  140. if not step_trace_files:
  141. # try to find step trace files under $profiler_dir/data
  142. profiler_dir = os.path.join(profiler_dir, 'data')
  143. step_trace_files = self._search_file(profiler_dir)
  144. if not step_trace_files:
  145. raise ProfilerPathErrorException('Training trace file does not exist.')
  146. return step_trace_files
  147. @staticmethod
  148. def _search_file(input_dir):
  149. """Search step trace file under specific input directory."""
  150. # validate input_dir
  151. if not os.path.isdir(input_dir):
  152. raise ProfilerPathErrorException(
  153. '{} does not exist or is not a dir'.format(input_dir)
  154. )
  155. # get step trace files
  156. files = os.listdir(input_dir)
  157. step_trace_files = list(
  158. filter(
  159. lambda file: file.startswith('training_trace') and not file.endswith('.done'),
  160. files
  161. )
  162. )
  163. # validate result
  164. if len(step_trace_files) > 1:
  165. # the format of file name is like
  166. # `training_trace.46.dev.profiler_default_tag.$id.slice_$number`
  167. # use the $number as the sorted key
  168. try:
  169. step_trace_files.sort(key=lambda path: int(path.rsplit('_', 1)[-1]))
  170. except ValueError as err:
  171. log.warning("Unable to parse file names: %s. %s", step_trace_files, err)
  172. step_trace_files = []
  173. file_paths = [os.path.join(input_dir, file) for file in step_trace_files]
  174. log.info("Find %d step trace files.", len(file_paths))
  175. return file_paths
  176. def _parse(self, source_files):
  177. """Parse source step trace files."""
  178. log.info("Start to parse step trace file.")
  179. event_info = {}
  180. for source_file in source_files:
  181. source_file = validate_and_normalize_path(source_file)
  182. with open(source_file, 'rb') as handler:
  183. content = handler.read()
  184. for step_trace in self._get_next_step_trace(content, event_info):
  185. if self._skip_first_step:
  186. self._skip_first_step = False
  187. continue
  188. self._record_trace_event(step_trace)
  189. self._record_average_info()
  190. log.info("Finish to parse step trace file.")
  191. def _get_next_step_trace(self, content, event_info):
  192. """
  193. Get next step trace info.
  194. Args:
  195. content (bytes): The input step trace info.
  196. event_info (dict): The event info.
  197. Returns:
  198. Generator, return the step trace one by one.
  199. """
  200. for pos in range(0, len(content), 20):
  201. next_event = self._get_trace_struct(content[pos:pos + self._event_size])
  202. self._construct_event_info(next_event, event_info)
  203. if event_info.get('end'):
  204. yield event_info
  205. def _get_trace_struct(self, bin_info):
  206. """Translate event info to StepTraceStruct."""
  207. if len(bin_info) == self._event_size:
  208. parsed_info = struct.unpack('=QHHQ', bin_info)
  209. return StepTraceStruct(*parsed_info)
  210. return None
  211. def _construct_event_info(self, next_event, event_info):
  212. """Construct event info according to next_event."""
  213. min_job_id = 255
  214. step_flag: bool = lambda tag: tag > min_job_id or tag == 0
  215. end_flag: bool = lambda tag: tag == min_job_id
  216. fp_flag: bool = lambda tag: tag == self._fp_tag
  217. bp_flag: bool = lambda tag: tag == self._bp_tag
  218. def _on_step_event():
  219. """Handle step event."""
  220. self._validate_tag_id(tag_id)
  221. start_time = event_info.get('end', '-')
  222. event_info.clear()
  223. event_info['start'] = start_time
  224. event_info['reduce'] = {}
  225. def _on_reduce_event(reduce_tag_id):
  226. """Handle reduce event."""
  227. stream_id = next_event.stream_id
  228. if event_info['reduce'].get(stream_id):
  229. event_info['reduce'][stream_id].append((reduce_tag_id, sys_count))
  230. else:
  231. event_info['reduce'][stream_id] = [(reduce_tag_id, sys_count)]
  232. tag_id = next_event.tag_id
  233. sys_count = next_event.sys_count
  234. if end_flag(tag_id):
  235. event_info['end'] = sys_count
  236. elif step_flag(tag_id):
  237. _on_step_event()
  238. elif fp_flag(tag_id):
  239. event_info['fp'] = sys_count
  240. elif bp_flag(tag_id):
  241. event_info['bp'] = sys_count
  242. else:
  243. _on_reduce_event(tag_id)
  244. def _validate_tag_id(self, job_id):
  245. """Check the job id in source step trace file is same as user set."""
  246. if not self._job_id:
  247. self._job_id = job_id
  248. elif self._job_id != job_id:
  249. raise JobIdMismatchException()
  250. def _record_trace_event(self, step_trace):
  251. """Record trace event."""
  252. self._step_num += 1
  253. start_time = step_trace.get('start')
  254. end_time = step_trace.get('end')
  255. fp_time = step_trace.get('fp')
  256. bp_time = step_trace.get('bp')
  257. if not (start_time and end_time and fp_time and bp_time):
  258. log.warning("The step %d lacks basic time.", self._step_num)
  259. return
  260. if start_time == '-':
  261. start_time = fp_time
  262. row_data = {
  263. 'step_num': self._step_num,
  264. 'start_point': start_time,
  265. 'end_point': end_time,
  266. 'total': end_time - start_time,
  267. 'fp_point': fp_time,
  268. 'bp_point': bp_time,
  269. 'iteration_interval': fp_time - start_time,
  270. 'fp_and_bp': bp_time - fp_time,
  271. 'tail': end_time - bp_time
  272. }
  273. # update reduce info
  274. self._update_reduce_info(step_trace, row_data)
  275. # save the row data
  276. if not self._header:
  277. self._header = list(row_data.keys())
  278. row_data_list = [row_data.get(header_name, 0) for header_name in self._header]
  279. self._result.append(row_data_list)
  280. def _update_reduce_info(self, step_trace, row_data):
  281. """Extract reduce info."""
  282. reduce_time = step_trace.get('reduce', {})
  283. for stream_id, time_points in reduce_time.items():
  284. time_point_num = len(time_points)
  285. if time_point_num % 2:
  286. log.warning("Stream %d has %d reduce time points.", stream_id, time_point_num)
  287. continue
  288. for index, point_id in enumerate(range(0, time_point_num, 2)):
  289. field_name = f'stream_{stream_id}_{index}'
  290. reduce_info = self._get_single_reduce_event_info(
  291. field_name, time_points[point_id], time_points[point_id + 1])
  292. row_data.update(reduce_info)
  293. def _get_single_reduce_event_info(self, field_name, start_point, end_point):
  294. """
  295. Get single reduce info.
  296. Args:
  297. field_name (str): The field name.
  298. start_point (Tuple[int, int]): Start point time info, including (tag_id, sys_count).
  299. end_point (Tuple[int, int]): End point time info, including (tag_id, sys_count).
  300. Returns:
  301. dict, reduce info.
  302. """
  303. reduce_info = {}
  304. if end_point[0] - start_point[0] != 1 or end_point[0] % 2:
  305. log.warning("Unmatched reduce event <%s, %s>.", start_point, end_point)
  306. return reduce_info
  307. op_type = self._tag_map.get(start_point[0])
  308. # append field name with op type.
  309. if not op_type:
  310. log.warning("Can't recognize the inner type for point tag: %d.", start_point[0])
  311. field_name += '_parallel'
  312. else:
  313. field_name += '_' + op_type
  314. reduce_info[field_name] = end_point[1] - start_point[1]
  315. reduce_info[field_name + '_start_point'] = start_point[1]
  316. reduce_info[field_name + '_end_point'] = end_point[1]
  317. return reduce_info
  318. def _record_average_info(self):
  319. """Calculate average info."""
  320. result_size = len(self._result)
  321. # calculate average data for each column in result data
  322. average_data = [0] * len(self._header)
  323. if result_size >= 2:
  324. for row_info in self._result[1:]:
  325. average_data = [
  326. Decimal(i) + Decimal(j) for i, j in zip(row_info, average_data)
  327. ]
  328. average_data = [
  329. round((item / (result_size - 1))) for item in average_data
  330. ]
  331. # change step num info in average_data to None
  332. step_num_index = self._header.index('step_num')
  333. average_data[step_num_index] = '-'
  334. self._result.append(average_data)
  335. log.info("Finish add average info for step trace.")
  336. def _save(self):
  337. log.info("Start to save step trace file.")
  338. if not self._header:
  339. return
  340. with open(self._output_path, 'w') as file_handle:
  341. csv_writer = csv.writer(file_handle)
  342. csv_writer.writerow(self._header)
  343. for row_data in self._result:
  344. csv_writer.writerow(row_data)
  345. os.chmod(self._output_path, stat.S_IREAD)