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 24 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  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, ProfilerRawFileException
  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 BaseStepTraceParser:
  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. is_training_mode (bool): Whether in training mode or not.
  41. """
  42. def __init__(self, input_dir, output_file_path, job_id=0, skip_first_step=False, is_training_mode=True):
  43. self._input_dir = input_dir
  44. self._output_path = output_file_path
  45. self._job_id = job_id
  46. self._skip_first_step = skip_first_step
  47. self._result = []
  48. self._header = []
  49. self._step_num = 0
  50. self._tag_map = {}
  51. self._is_training_mode = is_training_mode
  52. self._step_end_tag_id = 255
  53. @property
  54. def output_file(self):
  55. """The property of step trace header."""
  56. file_name = self._output_path.rsplit('/', 2)
  57. return file_name[-1] if len(file_name) == 3 else ''
  58. def show(self):
  59. """The property of step trace info."""
  60. summary_info = {}
  61. if self._result:
  62. summary_info = get_summary_for_step_trace(self._result[-1], self._header, self._is_training_mode)
  63. summary_info['total_steps'] = len(self._result) - 1
  64. print('\nStep trace summary info (unit: syscnt):')
  65. print(summary_info)
  66. print('\nThe step trace parse result saves under ${summary_dir}/profiler/%s'
  67. % self.output_file)
  68. def parse_and_save(self):
  69. """Parse step trace files and save the result."""
  70. try:
  71. source_files = self._get_step_trace_files()
  72. self._parse(source_files)
  73. self._save()
  74. except IOError as err:
  75. log.warning(err)
  76. raise ProfilerIOException()
  77. else:
  78. log.info("Finish to save intermediate result for step trace file.")
  79. def record_point_info(self, point_info, output_path):
  80. """
  81. Record point info into json.
  82. Args:
  83. point_info (dict): The point info about tag id and relative op name.
  84. output_path (str): The output path for saving point info.
  85. Returns:
  86. dict, parsed point info.
  87. """
  88. def update_tag_op_type_map(self, point_info):
  89. """
  90. update the map from tag id to op type.
  91. Args:
  92. point_info (dict): The point info about tag id and relative op name.
  93. """
  94. self._get_step_trace_files()
  95. self._get_step_end_tag_id()
  96. tag_map = {}
  97. for tag, op_name in point_info.items():
  98. op_type = self._get_op_type(tag, op_name)
  99. tag_map[tag] = op_type
  100. log.info("Get tag types for step trace analysis: %s", tag_map)
  101. self._tag_map = tag_map
  102. def _get_op_type(self, tag, name):
  103. """
  104. Get op type from tag and name.
  105. Args:
  106. tag (int): The tag id.
  107. name (str): The op name.
  108. Returns:
  109. str, the op type or communication op name.
  110. """
  111. tag_map = {self._fp_tag: 'fp', self._bp_tag: 'bp', self._step_end_tag_id: 'end'}
  112. # get solid tag type
  113. op_type = tag_map.get(tag, '')
  114. if op_type:
  115. return op_type
  116. # check if the tag is step tag.
  117. if tag > self._step_end_tag_id or tag == 0:
  118. return 'start'
  119. # analyze the reduce tag
  120. op_name = name.rsplit('/', 1)[-1]
  121. if not op_name:
  122. log.warning("Unexpected op name:%s", name)
  123. return op_name
  124. def _get_step_trace_files(self):
  125. """Get step trace files."""
  126. return self._input_dir
  127. @staticmethod
  128. def _search_file(input_dir):
  129. """Search step trace file under specific input directory."""
  130. # validate input_dir
  131. if not os.path.isdir(input_dir):
  132. raise ProfilerPathErrorException(
  133. '{} does not exist or is not a dir'.format(input_dir)
  134. )
  135. # get step trace files
  136. files = os.listdir(input_dir)
  137. step_trace_files = list(
  138. filter(
  139. lambda file: file.startswith('training_trace') and not file.endswith('.done'),
  140. files
  141. )
  142. )
  143. # validate result
  144. if len(step_trace_files) > 1:
  145. # the format of file name is like
  146. # `training_trace.46.dev.profiler_default_tag.$id.slice_$number`
  147. # use the $number as the sorted key
  148. try:
  149. step_trace_files.sort(key=lambda path: int(path.rsplit('_', 1)[-1]))
  150. except ValueError as err:
  151. log.warning("Unable to parse file names: %s. %s", step_trace_files, err)
  152. step_trace_files = []
  153. file_paths = [os.path.join(input_dir, file) for file in step_trace_files]
  154. log.info("Find %d step trace files.", len(file_paths))
  155. return file_paths
  156. def _parse(self, source_files):
  157. """Parse source step trace files."""
  158. def _get_next_step_trace(self, content, event_info):
  159. """
  160. Get next step trace info.
  161. Args:
  162. content (bytes): The input step trace info.
  163. event_info (dict): The event info.
  164. Returns:
  165. Generator, return the step trace one by one.
  166. """
  167. for pos in range(0, len(content), 20):
  168. next_event = self._get_trace_struct(content[pos:pos + self._event_size])
  169. self._construct_event_info(next_event, event_info)
  170. if event_info.get('end'):
  171. yield event_info
  172. def _get_trace_struct(self, bin_info):
  173. """Translate event info to StepTraceStruct."""
  174. if len(bin_info) == self._event_size:
  175. parsed_info = struct.unpack('=QHHQ', bin_info)
  176. return StepTraceStruct(*parsed_info)
  177. return None
  178. def _construct_event_info(self, next_event, event_info):
  179. """Construct event info according to next_event."""
  180. min_job_id = self._step_end_tag_id
  181. step_flag: bool = lambda tag: tag > min_job_id or tag == 0
  182. end_flag: bool = lambda tag: tag == min_job_id
  183. fp_flag: bool = lambda tag: tag == self._fp_tag
  184. bp_flag: bool = lambda tag: tag == self._bp_tag
  185. def _on_step_event():
  186. """Handle step event."""
  187. self._validate_tag_id(tag_id)
  188. start_time = event_info.get('end', '-')
  189. event_info.clear()
  190. event_info['start'] = start_time
  191. event_info['reduce'] = {}
  192. def _on_reduce_event(reduce_tag_id):
  193. """Handle reduce event."""
  194. stream_id = next_event.stream_id
  195. if event_info['reduce'].get(stream_id):
  196. event_info['reduce'][stream_id].append((reduce_tag_id, sys_count))
  197. else:
  198. event_info['reduce'][stream_id] = [(reduce_tag_id, sys_count)]
  199. tag_id = next_event.tag_id
  200. sys_count = next_event.sys_count
  201. if end_flag(tag_id):
  202. event_info['end'] = sys_count
  203. elif step_flag(tag_id):
  204. _on_step_event()
  205. elif fp_flag(tag_id):
  206. event_info['fp'] = sys_count
  207. elif bp_flag(tag_id):
  208. event_info['bp'] = sys_count
  209. else:
  210. _on_reduce_event(tag_id)
  211. def _validate_tag_id(self, job_id):
  212. """Check the job id in source step trace file is same as user set."""
  213. if not self._job_id:
  214. self._job_id = job_id
  215. elif self._job_id != job_id:
  216. raise JobIdMismatchException()
  217. def _record_trace_event(self, step_trace):
  218. """Record trace event."""
  219. self._step_num += 1
  220. start_time = step_trace.get('start')
  221. end_time = step_trace.get('end')
  222. fp_time = step_trace.get('fp')
  223. bp_time = step_trace.get('bp')
  224. if not (start_time and end_time and fp_time and bp_time):
  225. log.warning("The step %d lacks basic time.", self._step_num)
  226. return
  227. if start_time == '-':
  228. start_time = fp_time
  229. row_data = {
  230. 'step_num': self._step_num,
  231. 'start_point': start_time,
  232. 'end_point': end_time,
  233. 'total': end_time - start_time,
  234. 'fp_point': fp_time,
  235. 'bp_point': bp_time,
  236. 'iteration_interval': fp_time - start_time,
  237. 'fp_and_bp': bp_time - fp_time,
  238. 'tail': end_time - bp_time
  239. }
  240. # update reduce info
  241. self._update_reduce_info(step_trace, row_data)
  242. # save the row data
  243. if not self._header:
  244. self._header = list(row_data.keys())
  245. row_data_list = [row_data.get(header_name, 0) for header_name in self._header]
  246. self._result.append(row_data_list)
  247. def _update_reduce_info(self, step_trace, row_data):
  248. """Extract reduce info."""
  249. reduce_time = step_trace.get('reduce', {})
  250. for stream_id, time_points in reduce_time.items():
  251. time_point_num = len(time_points)
  252. if time_point_num % 2:
  253. log.warning("Stream %d has %d reduce time points.", stream_id, time_point_num)
  254. continue
  255. for index, point_id in enumerate(range(0, time_point_num, 2)):
  256. field_name = f'stream_{stream_id}_{index}'
  257. reduce_info = self._get_single_reduce_event_info(
  258. field_name, time_points[point_id], time_points[point_id + 1])
  259. row_data.update(reduce_info)
  260. def _get_single_reduce_event_info(self, field_name, start_point, end_point):
  261. """
  262. Get single reduce info.
  263. Args:
  264. field_name (str): The field name.
  265. start_point (Tuple[int, int]): Start point time info, including (tag_id, sys_count).
  266. end_point (Tuple[int, int]): End point time info, including (tag_id, sys_count).
  267. Returns:
  268. dict, reduce info.
  269. """
  270. ret_dict = {}
  271. return ret_dict
  272. def _record_average_info(self):
  273. """Calculate average info."""
  274. result_size = len(self._result)
  275. # calculate average data for each column in result data
  276. average_data = [0] * len(self._header)
  277. if result_size >= 2:
  278. for row_info in self._result[1:]:
  279. average_data = [
  280. Decimal(i) + Decimal(j) for i, j in zip(row_info, average_data)
  281. ]
  282. average_data = [
  283. round((item / (result_size - 1))) for item in average_data
  284. ]
  285. # change step num info in average_data to None
  286. step_num_index = self._header.index('step_num')
  287. average_data[step_num_index] = '-'
  288. self._result.append(average_data)
  289. log.info("Finish add average info for step trace.")
  290. def _save(self):
  291. """save step trace file."""
  292. BP_POINT, TAIL, FP_DURATION = 5, -1, -2
  293. log.info("Start to save step trace file.")
  294. if not self._header:
  295. return
  296. try:
  297. with open(self._output_path, 'w') as file_handle:
  298. csv_writer = csv.writer(file_handle)
  299. if not self._is_training_mode:
  300. self._header[FP_DURATION] = 'fp'
  301. self._header = self._header[:BP_POINT] + self._header[BP_POINT+1:TAIL]
  302. csv_writer.writerow(self._header)
  303. for row_data in self._result:
  304. if not self._is_training_mode:
  305. row_data[FP_DURATION] += row_data[TAIL]
  306. row_data = row_data[:BP_POINT] + row_data[BP_POINT+1:TAIL]
  307. csv_writer.writerow(row_data)
  308. os.chmod(self._output_path, stat.S_IRUSR)
  309. except (IOError, OSError) as err:
  310. log.warning('Failed to save step trace raw info. %s', err)
  311. raise ProfilerIOException
  312. class GpuStepTraceParser(BaseStepTraceParser):
  313. """The parser for gpu step trace data."""
  314. def record_point_info(self, source_file, output_path):
  315. """
  316. Record point info into json.
  317. Args:
  318. source_file (str): The file path of step trace original data.
  319. output_path (str): The output path for saving point info.
  320. Returns:
  321. dict, parsed point info.
  322. """
  323. fp_start, bp_end = 0, 1
  324. try:
  325. with open(source_file, 'r') as f:
  326. lines = f.readlines()
  327. fp_start_name = lines[fp_start].split()[0]
  328. bp_end_name = lines[bp_end].split()[0]
  329. except (IOError, OSError) as err:
  330. log.warning(f'Failed to read {source_file}', err)
  331. raise ProfilerIOException
  332. if self._is_training_mode:
  333. points = {
  334. 'fp_start': fp_start_name,
  335. 'bp_end': bp_end_name
  336. }
  337. else:
  338. points = {
  339. 'fp_start': fp_start_name,
  340. }
  341. if os.path.exists(output_path):
  342. return points
  343. try:
  344. with open(output_path, 'w') as json_file:
  345. json.dump(points, json_file)
  346. os.chmod(output_path, stat.S_IRUSR)
  347. except (IOError, OSError) as err:
  348. log.warning('Failed to save point info. %s', err)
  349. raise ProfilerIOException
  350. return points
  351. def _get_step_trace_files(self):
  352. """Get step trace files."""
  353. return self._input_dir
  354. def _parse(self, source_file):
  355. """Parse source step trace files."""
  356. log.info("Start to parse step trace file.")
  357. fp_start, bp_end, iter_end, iter_start = 0, 1, 2, 3
  358. reduce_start = 4
  359. start_time, end_time = 0, 1
  360. STEP_TRACE_POINT_COUNT = 3
  361. source_file = validate_and_normalize_path(source_file)
  362. try:
  363. with open(source_file, 'r') as f:
  364. lines = f.readlines()
  365. if len(lines) < STEP_TRACE_POINT_COUNT:
  366. raise ProfilerRawFileException(
  367. f"Failed to parse {source_file} file. The FP_POINT/BP_POINT/ITER_END_POINT "
  368. f"do not recognized correctly. Try to set the environment variable'PROFILING_FP_START' "
  369. f"and 'PROFILING_BP_END' to solve this problem. For example, "
  370. f"'export PROFILING_FP_START=Default/xxx/Conv2d-op1' ")
  371. step_trace_info_all = [line.strip().split()[1:] for line in lines]
  372. num_of_step = len(step_trace_info_all[0])
  373. for step_trace_point in step_trace_info_all:
  374. if len(step_trace_point) != num_of_step:
  375. raise ProfilerRawFileException(
  376. f"Failed to parse {source_file} file. Due to the profiled "
  377. f"step_num of FP/BP/ITER_END Point are not equal")
  378. iter_start_info = [step_trace_info_all[fp_start][0]] + \
  379. step_trace_info_all[iter_end][:num_of_step]
  380. step_trace_info_all.insert(iter_start, iter_start_info)
  381. except (IOError, OSError) as err:
  382. log.warning(f'Failed to read {source_file}', err)
  383. raise ProfilerIOException
  384. for step_num in range(num_of_step):
  385. step_trace = {
  386. 'start': int(step_trace_info_all[iter_start][step_num].split(',')[start_time]),
  387. 'fp': int(step_trace_info_all[fp_start][step_num].split(',')[start_time]),
  388. 'bp': int(step_trace_info_all[bp_end][step_num].split(',')[end_time]),
  389. 'end': int(step_trace_info_all[iter_end][step_num].split(',')[end_time]),
  390. 'reduce': {}
  391. }
  392. num_of_step_point = len(step_trace_info_all)
  393. if num_of_step_point > reduce_start:
  394. reduce_info = {}
  395. reduce_time_info = []
  396. for reduce_idx in range(reduce_start, num_of_step_point):
  397. cur_reduce_time = step_trace_info_all[reduce_idx][step_num]
  398. reduce_time_info += cur_reduce_time.split(',')
  399. reduce_info['ops'] = reduce_time_info
  400. step_trace['reduce'] = reduce_info
  401. self._record_trace_event(step_trace)
  402. self._record_average_info()
  403. log.info("Finish to parse step trace file.")
  404. def _get_single_reduce_event_info(self, field_name, start_point, end_point):
  405. """
  406. Get single reduce info.
  407. Args:
  408. field_name (str): The field name.
  409. start_point (str): Start point time.
  410. end_point (str): End point time.
  411. Returns:
  412. dict, reduce info.
  413. """
  414. reduce_info = {}
  415. op_type = 'AllReduce'
  416. # append field name with op type.
  417. field_name += '_' + op_type
  418. reduce_info[field_name] = int(end_point) - int(start_point)
  419. reduce_info[field_name + '_start_point'] = start_point
  420. reduce_info[field_name + '_end_point'] = end_point
  421. return reduce_info
  422. class AscendStepTraceParser(BaseStepTraceParser):
  423. """The parser for ascend step trace data."""
  424. _event_size = 20
  425. _fp_tag = 1
  426. _bp_tag = 2
  427. _step_trace_files = []
  428. def record_point_info(self, point_info, output_path):
  429. """
  430. Record point info into json.
  431. Args:
  432. point_info (dict): The point info about tag id and relative op name.
  433. output_path (str): The output path for saving point info.
  434. Returns:
  435. dict, parsed point info.
  436. """
  437. if self._is_training_mode:
  438. points = {
  439. 'fp_start': point_info.get(self._fp_tag, ''),
  440. 'bp_end': point_info.get(self._bp_tag, '')
  441. }
  442. else:
  443. points = {
  444. 'fp_start': point_info.get(self._fp_tag, ''),
  445. }
  446. if os.path.exists(output_path):
  447. return points
  448. try:
  449. with open(output_path, 'w') as json_file:
  450. json.dump(points, json_file)
  451. os.chmod(output_path, stat.S_IRUSR)
  452. except (IOError, OSError) as err:
  453. log.warning('Failed to save point info. %s', err)
  454. raise ProfilerIOException
  455. return points
  456. def _get_step_trace_files(self):
  457. """Get step trace files."""
  458. # step trace files may under $profiler_dir or $profiler_dir/data
  459. if self._step_trace_files:
  460. return self._step_trace_files
  461. profiler_dir = self._input_dir
  462. step_trace_files = self._search_file(profiler_dir)
  463. if not step_trace_files:
  464. # try to find step trace files under $profiler_dir/data
  465. profiler_dir = os.path.join(profiler_dir, 'data')
  466. step_trace_files = self._search_file(profiler_dir)
  467. if not step_trace_files:
  468. raise ProfilerPathErrorException('Training trace file does not exist.')
  469. self._step_trace_files = step_trace_files
  470. return step_trace_files
  471. def _get_step_end_tag_id(self):
  472. """
  473. Get step end tag id.This id is 255 before 2020.12.16,and 65535 now.
  474. File is an old version if there is no 65535 tag id, or it is a new version.
  475. """
  476. if not self._step_trace_files:
  477. return
  478. step_num = 0
  479. source_file = validate_and_normalize_path(self._step_trace_files[0])
  480. try:
  481. with open(source_file, 'rb') as handler:
  482. content = handler.read()
  483. for pos in range(0, len(content), 20):
  484. next_event = self._get_trace_struct(content[pos:pos + self._event_size])
  485. # 1 means bp_start.
  486. if next_event.tag_id == 1:
  487. step_num += 1
  488. # Step end tag id is 65535 in the new version.
  489. if next_event.tag_id == 65535:
  490. self._step_end_tag_id = next_event.tag_id
  491. # We only search the first step to find if there is 65535 tag id.
  492. if step_num == 2:
  493. break
  494. except (IOError, OSError) as err:
  495. log.warning(f'Failed to read {source_file} while get end tag id', err)
  496. raise ProfilerIOException
  497. def _parse(self, source_files):
  498. """Parse source step trace files."""
  499. log.info("Start to parse step trace file.")
  500. event_info = {}
  501. for source_file in source_files:
  502. source_file = validate_and_normalize_path(source_file)
  503. try:
  504. with open(source_file, 'rb') as handler:
  505. content = handler.read()
  506. for step_trace in self._get_next_step_trace(content, event_info):
  507. if self._skip_first_step:
  508. self._skip_first_step = False
  509. continue
  510. self._record_trace_event(step_trace)
  511. except (IOError, OSError) as err:
  512. log.warning(f'Failed to read {source_file}', err)
  513. raise ProfilerIOException
  514. self._record_average_info()
  515. log.info("Finish to parse step trace file.")
  516. def _get_single_reduce_event_info(self, field_name, start_point, end_point):
  517. """
  518. Get single reduce info.
  519. Args:
  520. field_name (str): The field name.
  521. start_point (Tuple[int, int]): Start point time info, including (tag_id, sys_count).
  522. end_point (Tuple[int, int]): End point time info, including (tag_id, sys_count).
  523. Returns:
  524. dict, reduce info.
  525. """
  526. reduce_info = {}
  527. if end_point[0] - start_point[0] != 1 or end_point[0] % 2:
  528. log.warning("Unmatched reduce event <%s, %s>.", start_point, end_point)
  529. return reduce_info
  530. op_type = self._tag_map.get(start_point[0])
  531. # append field name with op type.
  532. if not op_type:
  533. log.warning("Can't recognize the inner type for point tag: %d.", start_point[0])
  534. field_name += '_parallel'
  535. else:
  536. field_name += '_' + op_type
  537. reduce_info[field_name] = end_point[1] - start_point[1]
  538. reduce_info[field_name + '_start_point'] = start_point[1]
  539. reduce_info[field_name + '_end_point'] = end_point[1]
  540. return reduce_info