| @@ -86,3 +86,5 @@ build/* | |||
| output/ | |||
| !output/README.md | |||
| mindinsight/ui/public/static/js/graphvizlib.wasm | |||
| @@ -1,3 +1,3 @@ | |||
| [submodule "third_party/securec"] | |||
| path = third_party/securec | |||
| url = https://gitee.com/openeuler/bounds_checking_function.git | |||
| url = https://gitee.com/openeuler/libboundscheck.git | |||
| @@ -11,6 +11,10 @@ such as computation graph, training process metrics, etc. | |||
| Provide visualization of model parameters information, | |||
| such as training data, model accuracy, etc. | |||
| - Visualization of training performance: | |||
| Provide visualization of training performance information, such as operator execution time, | |||
| data input pipeline performance, etc. | |||
| # Index | |||
| @@ -91,6 +95,10 @@ The GUI of MindInsight displays the pipeline of dataset processing and augmentat | |||
| The GUI of MindInsight displays the parameters and operations of the dataset processing and augmentation. | |||
| ### Performance visualization | |||
| The GUI of MindInsight displays the performance data of the neural networks. | |||
| # Installation | |||
| See [Install MindInsight](https://www.mindspore.cn/install/en). | |||
| @@ -1,5 +1,35 @@ | |||
| ## MindInsight | |||
| # Release 0.5.0-beta | |||
| ## Major Features and Improvements | |||
| * MindSpore Profiler | |||
| * Provide performance analyse tool for the input data pipeline. | |||
| * Provide timeline analyse tool, which can show the detail of the streams/tasks. | |||
| * Provide a tool to visualize the step trace information, which can be used to analyse the general performance of the neural network in each phase. | |||
| * Provide profiling guides for the users to find the performance bottlenecks quickly. | |||
| * CPU summary operations support for CPU summary data. | |||
| * Over threshold warn support in scalar training dashboard. | |||
| * Provide more user-friendly callback function for visualization | |||
| * Provide unified callback `SummaryCollector` to log most commonly visualization event. | |||
| * Discard the original visualization callback `SummaryStep`, `TrainLineage` and `EvalLineage`. | |||
| * `SummaryRecord` provide new API `add_value` to collect data into cache for summary persistence. | |||
| * `SummaryRecord` provide new API `set_mode` to distinguish summary persistence mode at different stages. | |||
| * MindConverter supports conversion of more operators and networks, and improves its ease of use. | |||
| ## Bugfixes | |||
| * Fix FileNotFound exception by adding robust check for summary watcher ([!281](https://gitee.com/mindspore/mindinsight/pulls/281)). | |||
| * UI fix operator table sort jump problem ([!283](https://gitee.com/mindspore/mindinsight/pulls/283)). | |||
| * Dataset serializer return schema json str when schema type is `mindspore.dataset.engine.Schema` ([!2185](https://gitee.com/mindspore/mindspore/pulls/2185)). | |||
| ## Thanks to our Contributors | |||
| Thanks goes to these wonderful people: | |||
| Chao Chen, Congli Gao, Ye Huang, Weifeng Huang, Zhenzhong Kou, Hongzhang Li, Longfei Li, Yongxiong Liang, Chongming Liu, Pengting Luo, Yanming Miao, Gongchang Ou, Yongxiu Qu, Hui Pan, Luyu Qiu, Junyan Qin, Kai Wen, Weining Wang, Yue Wang, Zhuanke Wu, Yifan Xia, Lihua Ye, Weibiao Yu, Ximiao Yu, Yunshu Zhang, Ting Zhao, Jianfeng Zhu. | |||
| Contributions of any kind are welcome! | |||
| # Release 0.3.0-alpha | |||
| ## Major Features and Improvements | |||
| @@ -21,7 +21,6 @@ import json | |||
| import os | |||
| from flask import Blueprint | |||
| from flask import Response | |||
| from flask import jsonify | |||
| from flask import request | |||
| from marshmallow import ValidationError | |||
| @@ -31,7 +30,8 @@ from mindinsight.datavisual.utils.tools import get_train_id, get_profiler_dir, t | |||
| from mindinsight.datavisual.utils.tools import unquote_args | |||
| from mindinsight.profiler.analyser.analyser_factory import AnalyserFactory | |||
| from mindinsight.profiler.analyser.minddata_analyser import MinddataAnalyser | |||
| from mindinsight.profiler.common.exceptions.exceptions import ProfilerFileNotFoundException | |||
| from mindinsight.profiler.common.exceptions.exceptions import ProfilerFileNotFoundException, \ | |||
| ProfilerDirNotFoundException | |||
| from mindinsight.profiler.common.util import analyse_device_list_from_profiler_dir | |||
| from mindinsight.profiler.common.validator.validate import validate_condition, validate_ui_proc | |||
| from mindinsight.profiler.common.validator.validate import validate_minddata_pipeline_condition | |||
| @@ -40,6 +40,7 @@ from mindinsight.profiler.common.validator.validate_path import \ | |||
| from mindinsight.profiler.common.validator.validate_path import validate_and_normalize_profiler_path | |||
| from mindinsight.profiler.proposer.compose_proposer import ComposeProposal | |||
| from mindinsight.utils.exceptions import ParamValueError | |||
| from mindinsight.backend.application import CustomResponse | |||
| BLUEPRINT = Blueprint("profile", __name__, url_prefix=settings.URL_PATH_PREFIX+settings.API_PREFIX) | |||
| @@ -71,6 +72,7 @@ def get_profile_op_info(): | |||
| validate_condition(search_condition) | |||
| device_id = search_condition.get("device_id", "0") | |||
| to_int(device_id, 'device_id') | |||
| profiler_dir_abs = os.path.join(settings.SUMMARY_BASE_DIR, train_id, profiler_dir) | |||
| try: | |||
| profiler_dir_abs = validate_and_normalize_path(profiler_dir_abs, "profiler") | |||
| @@ -193,6 +195,7 @@ def get_queue_info(): | |||
| profile_dir = get_profiler_abs_dir(request) | |||
| device_id = unquote_args(request, "device_id") | |||
| to_int(device_id, 'device_id') | |||
| queue_type = unquote_args(request, "type") | |||
| queue_info = {} | |||
| @@ -219,6 +222,7 @@ def get_time_info(): | |||
| """ | |||
| profile_dir = get_profiler_abs_dir(request) | |||
| device_id = unquote_args(request, "device_id") | |||
| to_int(device_id, 'device_id') | |||
| op_type = unquote_args(request, "type") | |||
| time_info = { | |||
| @@ -250,6 +254,7 @@ def get_process_summary(): | |||
| """ | |||
| profile_dir = get_profiler_abs_dir(request) | |||
| device_id = unquote_args(request, "device_id") | |||
| to_int(device_id, 'device_id') | |||
| minddata_analyser = AnalyserFactory.instance().get_analyser( | |||
| 'minddata', profile_dir, device_id) | |||
| @@ -304,6 +309,7 @@ def get_profile_summary_proposal(): | |||
| device_id = get_device_id(request) | |||
| if not profiler_dir or not train_id: | |||
| raise ParamValueError("No profiler_dir or train_id.") | |||
| to_int(device_id, 'device_id') | |||
| profiler_dir_abs = os.path.join(settings.SUMMARY_BASE_DIR, train_id, profiler_dir) | |||
| try: | |||
| @@ -320,7 +326,7 @@ def get_profile_summary_proposal(): | |||
| proposal_obj = ComposeProposal(profiler_dir_abs, device_id, proposal_type_list) | |||
| proposal_info = proposal_obj.get_proposal(options) | |||
| # Use json.dumps for orderly return | |||
| return Response(json.dumps(proposal_info), mimetype='application/json') | |||
| return CustomResponse(json.dumps(proposal_info), mimetype='application/json') | |||
| @BLUEPRINT.route("/profile/minddata-pipeline/op-queue", methods=["POST"]) | |||
| @@ -360,6 +366,7 @@ def get_minddata_pipeline_op_queue_info(): | |||
| validate_minddata_pipeline_condition(condition) | |||
| device_id = condition.get("device_id", "0") | |||
| to_int(device_id, 'device_id') | |||
| analyser = AnalyserFactory.instance().get_analyser( | |||
| 'minddata_pipeline', profiler_dir_abs, device_id | |||
| ) | |||
| @@ -397,6 +404,7 @@ def get_minddata_pipeline_queue_info(): | |||
| raise ParamValueError("Invalid profiler dir.") | |||
| device_id = request.args.get('device_id', default='0') | |||
| to_int(device_id, 'device_id') | |||
| op_id = request.args.get('op_id', type=int) | |||
| if op_id is None: | |||
| raise ParamValueError("Invalid operator id or operator id does not exist.") | |||
| @@ -421,6 +429,9 @@ def get_timeline_summary(): | |||
| """ | |||
| summary_dir = request.args.get("dir") | |||
| profiler_dir = validate_and_normalize_profiler_path(summary_dir, settings.SUMMARY_BASE_DIR) | |||
| if not os.path.exists(profiler_dir): | |||
| msg = 'The profiler dir is not found!' | |||
| raise ProfilerDirNotFoundException(msg=msg) | |||
| device_id = request.args.get("device_id", default='0') | |||
| _ = to_int(device_id, 'device_id') | |||
| @@ -444,6 +455,9 @@ def get_timeline_detail(): | |||
| """ | |||
| summary_dir = request.args.get("dir") | |||
| profiler_dir = validate_and_normalize_profiler_path(summary_dir, settings.SUMMARY_BASE_DIR) | |||
| if not os.path.exists(profiler_dir): | |||
| msg = 'The profiler dir is not found!' | |||
| raise ProfilerDirNotFoundException(msg=msg) | |||
| device_id = request.args.get("device_id", default='0') | |||
| _ = to_int(device_id, 'device_id') | |||
| @@ -107,6 +107,15 @@ class TrainJobNotExistError(MindInsightException): | |||
| http_code=400) | |||
| class QueryStringContainsNullByteError(MindInsightException): | |||
| """Query string contains null byte error.""" | |||
| def __init__(self, error_detail): | |||
| error_msg = f"Query string contains null byte error. Detail: {error_detail}" | |||
| super(QueryStringContainsNullByteError, self).__init__(DataVisualErrors.QUERY_STRING_CONTAINS_NULL_BYTE, | |||
| error_msg, | |||
| http_code=400) | |||
| class PluginNotAvailableError(MindInsightException): | |||
| """The given plugin is not available.""" | |||
| def __init__(self, error_detail): | |||
| @@ -22,6 +22,7 @@ from pathlib import Path | |||
| from mindinsight.datavisual.common.log import logger | |||
| from mindinsight.datavisual.common.validation import Validation | |||
| from mindinsight.datavisual.utils.tools import Counter | |||
| from mindinsight.datavisual.utils.utils import contains_null_byte | |||
| from mindinsight.datavisual.common.exceptions import MaxCountExceededError | |||
| from mindinsight.utils.exceptions import FileSystemPermissionError | |||
| @@ -61,7 +62,7 @@ class SummaryWatcher: | |||
| >>> summary_watcher = SummaryWatcher() | |||
| >>> directories = summary_watcher.list_summary_directories('/summary/base/dir') | |||
| """ | |||
| if self._contains_null_byte(summary_base_dir=summary_base_dir): | |||
| if contains_null_byte(summary_base_dir=summary_base_dir): | |||
| return [] | |||
| relative_path = os.path.join('.', '') | |||
| @@ -148,25 +149,6 @@ class SummaryWatcher: | |||
| pass | |||
| self._update_summary_dict(summary_dict, summary_base_dir, subdir_relative_path, subdir_entry) | |||
| def _contains_null_byte(self, **kwargs): | |||
| """ | |||
| Check if arg contains null byte. | |||
| Args: | |||
| kwargs (Any): Check if arg contains null byte. | |||
| Returns: | |||
| bool, indicates if any arg contains null byte. | |||
| """ | |||
| for key, value in kwargs.items(): | |||
| if not isinstance(value, str): | |||
| continue | |||
| if '\x00' in value: | |||
| logger.warning('%s contains null byte \\x00.', key) | |||
| return True | |||
| return False | |||
| def _is_valid_summary_directory(self, summary_base_dir, relative_path): | |||
| """ | |||
| Check if the given summary directory is valid. | |||
| @@ -276,7 +258,7 @@ class SummaryWatcher: | |||
| >>> summary_watcher = SummaryWatcher() | |||
| >>> summaries = summary_watcher.is_summary_directory('/summary/base/dir', './job-01') | |||
| """ | |||
| if self._contains_null_byte(summary_base_dir=summary_base_dir, relative_path=relative_path): | |||
| if contains_null_byte(summary_base_dir=summary_base_dir, relative_path=relative_path): | |||
| return False | |||
| if not self._is_valid_summary_directory(summary_base_dir, relative_path): | |||
| @@ -371,7 +353,7 @@ class SummaryWatcher: | |||
| >>> summary_watcher = SummaryWatcher() | |||
| >>> summaries = summary_watcher.list_summaries('/summary/base/dir', './job-01') | |||
| """ | |||
| if self._contains_null_byte(summary_base_dir=summary_base_dir, relative_path=relative_path): | |||
| if contains_null_byte(summary_base_dir=summary_base_dir, relative_path=relative_path): | |||
| return [] | |||
| if not self._is_valid_summary_directory(summary_base_dir, relative_path): | |||
| @@ -19,7 +19,9 @@ from mindinsight.datavisual.common.log import logger | |||
| from mindinsight.datavisual.common import exceptions | |||
| from mindinsight.datavisual.common.enums import PluginNameEnum | |||
| from mindinsight.datavisual.common.enums import CacheStatus | |||
| from mindinsight.datavisual.common.exceptions import QueryStringContainsNullByteError | |||
| from mindinsight.datavisual.common.validation import Validation | |||
| from mindinsight.datavisual.utils.utils import contains_null_byte | |||
| from mindinsight.datavisual.processors.base_processor import BaseProcessor | |||
| from mindinsight.datavisual.data_transform.data_manager import DATAVISUAL_PLUGIN_KEY, DATAVISUAL_CACHE_KEY | |||
| @@ -57,6 +59,8 @@ class TrainTaskManager(BaseProcessor): | |||
| dict, refer to restful api. | |||
| """ | |||
| Validation.check_param_empty(train_id=train_id) | |||
| if contains_null_byte(train_id=train_id): | |||
| raise QueryStringContainsNullByteError("train job id: {} contains null byte.".format(train_id)) | |||
| if manual_update: | |||
| self._data_manager.cache_train_job(train_id) | |||
| @@ -14,6 +14,7 @@ | |||
| # ============================================================================ | |||
| """Utils.""" | |||
| import math | |||
| from mindinsight.datavisual.common.log import logger | |||
| def calc_histogram_bins(count): | |||
| @@ -45,3 +46,23 @@ def calc_histogram_bins(count): | |||
| return math.ceil(count / number_per_bucket) + 1 | |||
| return max_bins | |||
| def contains_null_byte(**kwargs): | |||
| """ | |||
| Check if arg contains null byte. | |||
| Args: | |||
| kwargs (Any): Check if arg contains null byte. | |||
| Returns: | |||
| bool, indicates if any arg contains null byte. | |||
| """ | |||
| for key, value in kwargs.items(): | |||
| if not isinstance(value, str): | |||
| continue | |||
| if '\x00' in value: | |||
| logger.warning('%s contains null byte \\x00.', key) | |||
| return True | |||
| return False | |||
| @@ -32,6 +32,7 @@ from mindinsight.mindconverter.common.log import logger | |||
| from mindinsight.mindconverter.common.exceptions import NodeTypeNotSupport | |||
| from mindinsight.mindconverter.forward_call import ForwardCall | |||
| LOG_FMT_INSERT = "[Insert] '%s' is inserted to the converted file." | |||
| LOG_FMT_CONVERT = "[Convert] '%s' is converted to '%s'." | |||
| LOG_FMT_CONVERT_WITH_TIPS = "[Convert] '%s' is converted to '%s'. %s" | |||
| LOG_FMT_NOT_CONVERT = "[UnConvert] '%s' didn't convert. %s" | |||
| @@ -54,16 +55,22 @@ class _ConvertReport: | |||
| def __init__(self, is_stub=False): | |||
| self._is_stub = is_stub | |||
| self._max_line = 0 | |||
| self._log = [] # report log, type is (severity, line, col, msg) | |||
| self._log_head = [] | |||
| self._log_body = [] # report log, type is (severity, line, col, msg) | |||
| def _add_log(self, severity, line, col, msg): | |||
| """Add log.""" | |||
| if self._is_stub: | |||
| return | |||
| if line is None and col is None: | |||
| self._log_head.append(msg) | |||
| return | |||
| if isinstance(line, int) and isinstance(col, int): | |||
| self._log.append((severity, line, col, msg)) | |||
| self._log_body.append((severity, line, col, msg)) | |||
| if self._max_line < line: | |||
| self._max_line = line | |||
| else: | |||
| raise TypeError('The parameter type is incorrect.') | |||
| def info(self, line, col, msg): | |||
| """Interface to add infer log""" | |||
| @@ -73,14 +80,24 @@ class _ConvertReport: | |||
| """Interface to add warning log""" | |||
| self._add_log(logging.WARNING, line, col, msg) | |||
| def header_msg(self, msg): | |||
| """Interface to add header message log""" | |||
| self._add_log(logging.INFO, None, None, msg) | |||
| def get_logs(self): | |||
| """Get convert logs""" | |||
| logs = [] | |||
| logs.extend(self._log_head) | |||
| # sort rule: line * self._max_line + col | |||
| self._log.sort(key=lambda log: log[1] * self._max_line + log[2]) | |||
| for log_info in self._log: | |||
| self._log_body.sort(key=lambda log: log[1] * self._max_line + log[2]) | |||
| for log_info in self._log_body: | |||
| log_info = "line %d:%d: %s" % (log_info[1], log_info[2], log_info[3]) | |||
| logs.append(log_info) | |||
| if logs: | |||
| # Deduplication for logs | |||
| if logs[-1] != log_info: | |||
| logs.append(log_info) | |||
| else: | |||
| logs.append(log_info) | |||
| return logs | |||
| @@ -262,7 +279,8 @@ class AstEditVisitor(ast.NodeVisitor): | |||
| new_func_name = 'construct' | |||
| if func_ast_node.name == old_func_name: | |||
| func_ast_node.name = new_func_name | |||
| self._process_log.info(func_ast_node.lineno, func_ast_node.col_offset, | |||
| real_line_number = self._get_real_line_number(func_ast_node) | |||
| self._process_log.info(real_line_number, func_ast_node.col_offset, | |||
| LOG_FMT_CONVERT % (old_func_name, new_func_name)) | |||
| def _convert_api(self): | |||
| @@ -297,7 +315,16 @@ class AstEditVisitor(ast.NodeVisitor): | |||
| if source_prefix: | |||
| pos = len(source_prefix) | |||
| source_code = pasta.dump(node) | |||
| return source_code[pos] | |||
| return source_code[pos:] | |||
| @staticmethod | |||
| def _get_real_line_number(node): | |||
| """Get the real line number of the node.""" | |||
| try: | |||
| line_number = node.lineno + len(node.decorator_list) | |||
| except AttributeError: | |||
| line_number = node.lineno | |||
| return line_number | |||
| def _replace_external_reference(self): | |||
| """ | |||
| @@ -349,6 +376,7 @@ class AstEditVisitor(ast.NodeVisitor): | |||
| insert_pos += 1 | |||
| else: | |||
| try: | |||
| # insert pos after the last one, if last one name is replaced. | |||
| replaced_with_node = names_replaced_with[src_name] | |||
| insert_pos = self._tree.body.index(replaced_with_node) + 1 | |||
| except ValueError: | |||
| @@ -359,6 +387,8 @@ class AstEditVisitor(ast.NodeVisitor): | |||
| for insert_pos, new_node in new_import_node.items(): | |||
| # Insert the node into the module | |||
| self._tree.body.insert(insert_pos + insert_cnt, new_node) | |||
| new_code = self._dump_without_prefix(new_node) | |||
| self._process_log.header_msg(LOG_FMT_INSERT % new_code.strip()) | |||
| insert_cnt += 1 | |||
| @staticmethod | |||
| @@ -445,8 +475,10 @@ class AstEditVisitor(ast.NodeVisitor): | |||
| is_include_sub_call = self._is_include_sub_call(call_func_node) | |||
| if is_include_sub_call: | |||
| # x.y().z splits to ['x.y()', 'z'] | |||
| name_attributes = call_name.rsplit('.', 1) | |||
| else: | |||
| # x.y.z splits to ['x', 'y', 'z'] | |||
| name_attributes = call_name.split('.') | |||
| # rewritten external module name | |||
| @@ -638,6 +670,55 @@ class AstEditVisitor(ast.NodeVisitor): | |||
| return new_code | |||
| @staticmethod | |||
| def _get_detail_prompt_msg(old_node, new_node): | |||
| """Get detail converted prompt information.""" | |||
| msg = None | |||
| if isinstance(old_node, ast.Call) and isinstance(new_node, ast.Call): | |||
| old_api_name = pasta.dump(old_node.func) | |||
| new_api_name = pasta.dump(new_node.func) | |||
| if new_api_name == old_api_name: | |||
| old_parameter_num = len(old_node.args) + len(old_node.keywords) | |||
| new_parameter_num = len(new_node.args) + len(new_node.keywords) | |||
| if old_parameter_num > 1: | |||
| msg = 'Parameters are converted.' | |||
| else: | |||
| if old_parameter_num == 0 and new_parameter_num == 0: | |||
| msg = 'The API name is converted to mindspore API' | |||
| else: | |||
| msg = 'Parameter is converted.' | |||
| return msg | |||
| def _convert_call(self, node, matched_api_name): | |||
| """"Convert the call node.""" | |||
| new_node = None | |||
| code = pasta.dump(node) | |||
| api_name = pasta.dump(node.func) | |||
| warning_info = get_prompt_info(matched_api_name) | |||
| if warning_info is None: | |||
| warning_info = '' | |||
| if matched_api_name in ALL_MAPPING: | |||
| logger.info("Line %3d start converting API: %s", node.lineno, api_name) | |||
| new_code = self.mapping_api(node) | |||
| if new_code != code: | |||
| try: | |||
| new_node = pasta.parse(new_code).body[0].value | |||
| # find the first call name | |||
| new_api_name = new_code[:new_code.find('(')] | |||
| detail_msg = self._get_detail_prompt_msg(node, new_node) | |||
| if detail_msg: | |||
| warning_info = detail_msg + ' ' + warning_info | |||
| except AttributeError: | |||
| new_node = pasta.parse(new_code).body[0] | |||
| new_api_name = new_code | |||
| self._process_log.info(node.lineno, node.col_offset, | |||
| LOG_FMT_CONVERT_WITH_TIPS % (api_name, new_api_name, warning_info)) | |||
| else: | |||
| logger.warning("Line %3d: found unsupported API: %s%s", node.lineno, api_name, warning_info) | |||
| self._process_log.warning(node.lineno, node.col_offset, LOG_FMT_NOT_CONVERT % (api_name, warning_info)) | |||
| return new_node | |||
| def visit_Call(self, node): | |||
| """Callback function when visit AST tree""" | |||
| code = pasta.dump(node) | |||
| @@ -655,26 +736,7 @@ class AstEditVisitor(ast.NodeVisitor): | |||
| new_code = code | |||
| matched_api_name, match_case = self.match_api(node.func, self._is_forward_function) | |||
| if match_case in [ApiMatchingEnum.API_INFER, ApiMatchingEnum.API_MATCHED]: | |||
| warning_info = get_prompt_info(matched_api_name) | |||
| if warning_info is None: | |||
| warning_info = '' | |||
| if matched_api_name in ALL_MAPPING: | |||
| logger.info("Line %3d start converting API: %s", node.lineno, api_name) | |||
| new_code = self.mapping_api(node) | |||
| if new_code != code: | |||
| try: | |||
| new_node = pasta.parse(new_code).body[0].value | |||
| # find the first call name | |||
| new_api_name = new_code[:new_code.find('(')] | |||
| except AttributeError: | |||
| new_node = pasta.parse(new_code).body[0] | |||
| new_api_name = new_code | |||
| self._process_log.info(node.lineno, node.col_offset, | |||
| LOG_FMT_CONVERT_WITH_TIPS % (api_name, new_api_name, warning_info)) | |||
| else: | |||
| logger.warning("Line %3d: found unsupported API: %s%s", node.lineno, api_name, warning_info) | |||
| self._process_log.warning(node.lineno, node.col_offset, LOG_FMT_NOT_CONVERT % (api_name, warning_info)) | |||
| new_node = self._convert_call(node, matched_api_name) | |||
| elif match_case in [ApiMatchingEnum.API_STANDARD, ApiMatchingEnum.API_FOUND]: | |||
| self._process_log.warning(node.lineno, node.col_offset, LOG_FMT_NOT_CONVERT % (api_name, '')) | |||
| else: | |||
| @@ -32,7 +32,7 @@ FUNC_MODULE = 'mindinsight.mindconverter.funcs' | |||
| class APIPt: | |||
| """Base API for args parse, and API for one frame.""" | |||
| def __init__(self, name: str, params: OrderedDict): | |||
| def __init__(self, name: str, params: dict): | |||
| self.name = name | |||
| self.params = OrderedDict() | |||
| @@ -45,7 +45,7 @@ class APIPt: | |||
| Trans value to str. | |||
| Args: | |||
| value (Union[str,Number,int]): Each value for params of OrderedDict. | |||
| value (Union[str,Number,int]): The value to convert. | |||
| Returns: | |||
| str, str type of value. | |||
| @@ -118,7 +118,7 @@ class APIPt: | |||
| class APIMs(APIPt): | |||
| """API for MindSpore""" | |||
| def __init__(self, name: str, params: OrderedDict, p_attrs=None): | |||
| def __init__(self, name: str, params: dict, p_attrs=None): | |||
| self.is_primitive = name.startswith('P.') | |||
| if self.is_primitive: | |||
| self.p_attrs = p_attrs if p_attrs else set() | |||
| @@ -450,7 +450,6 @@ UNSUPPORTED_WARN_INFOS = { | |||
| "F.one_hot": "Maybe could convert to mindspore.ops.operations.OneHot.", | |||
| "torch.bmm": "Maybe could convert to mindspore.ops.operations.BatchMatMul.", | |||
| "torch.cumsum": "Maybe could convert to mindspore.ops.operations.CumSum.", | |||
| "F.relu": "Maybe could convert to mindspore.ops.operations.ReLU.", | |||
| "F.pad": "Maybe could convert to mindspore.ops.operations.Pad.", | |||
| "F.softmax": "Maybe could convert to mindspore.ops.operations.Softmax.", | |||
| "torch.clamp": "Maybe could convert to mindspore.ops.composite.clip_by_value.", | |||
| @@ -65,8 +65,8 @@ Figure 2 displays the Step Trace page. The Step Trace detail will show the start | |||
| can also choose a specific step to see its step trace statistics. The graphs at the bottom of the page show how the execution time of Step Gap, Forward/Backward Propagation and | |||
| Step Tail changes according to different steps, it will help to decide whether we can optimize the performance of some stages. | |||
| *Notice:* MindSpore choose the Foward Start/Backward End Operators automatically, The names of the two operators are shown on the page. It is possible that the two operators are | |||
| not choosen as what the user expect. Users can choose the operators from the dumped execution graph, and specify the two operators manually by setting the `FP_POINT` and `BP_POINT` environment. | |||
| *Notice:* MindSpore choose the Forward Start/Backward End Operators automatically, The names of the two operators are shown on the page. Profiler do not guarantee that the two operators are | |||
| always chosen as the user's expectation. Users can choose the two operators according to the execution graph, and specify the them manually by setting the `FP_POINT` and `BP_POINT` environment variables. | |||
| For example: `export FP_POINT=fp32_vars/conv2d/conv2Dfp32_vars/BatchNorm/FusedBatchNorm_Reduce` and `export BP_POINT=loss_scale/gradients/AddN_70`. | |||
| ### Operator Performance Analysis | |||
| @@ -138,6 +138,17 @@ The Timeline component can display: | |||
| - The MindSpore stream split strategy for this neural network; | |||
| - The time of tasks executed on the device. | |||
| How to view the timeline: | |||
| To view the detailed information of the timeline, you can click the "Download" button to save the file with the timeline information locally, and then view it through the tool. | |||
| We recommend you to use Google plugin: chrome://tracing, or Perfetto tool: https://ui.perfetto.dev/#!viewer. | |||
| - Select one of the tools mentioned above, enter the address in the browser and press Enter; | |||
| - After entered the page, click the button to load the file to view the timeline. | |||
| - For chrome tracing, using "load" button in the upper left corner. | |||
| - For Perfetto, using "Open trace file" in the left column. | |||
| Users can get the most detailed information from the Timeline: | |||
| - From high level, users can analyse whether the stream split strategy can be optimized and whether the step tail is too long; | |||
| @@ -160,3 +171,5 @@ The Profiler has the following limitations now: | |||
| * Only programs running on Ascend chip is supported. | |||
| * To limit the data size generated by the Profiler, MindInsight suggests that for large neural network, the profiled steps should better below 10. | |||
| * The parse of Timeline data is time consuming, and several step's data is usually enough for analysis. In order to speed up the data parse and UI | |||
| display, Profiler will show at most 20M data (Contain 10+ step information for large networks). | |||
| @@ -34,6 +34,8 @@ class AicoreTypeAnalyser(BaseAnalyser): | |||
| ProfilerPathErrorException: If the profiling dir is invalid. | |||
| """ | |||
| _col_names = ['op_type', 'execution_time', 'execution_frequency', 'percent'] | |||
| _col_names_in_result = ['op_type', 'execution_time (ms)', | |||
| 'execution_frequency', 'percent'] | |||
| _file_name_aicore_type_time = 'aicore_intermediate_{}_type.csv' | |||
| def _load(self): | |||
| @@ -63,6 +65,18 @@ class AicoreTypeAnalyser(BaseAnalyser): | |||
| return self._default_filter(item, filter_condition) | |||
| self._result = list(filter(_inner_filter, self._data)) | |||
| def _organize_query_result(self): | |||
| """ | |||
| Organize the query result. | |||
| Returns: | |||
| dict, the query result. | |||
| """ | |||
| for item in self._result: | |||
| item[1] = float(format(item[1], '.6f')) | |||
| self._display_col_names = self._col_names_in_result[:] | |||
| return super()._organize_query_result() | |||
| def _convert_field_type(self, row): | |||
| """ | |||
| Convert the field type to the specific type. | |||
| @@ -88,8 +102,10 @@ class AicoreDetailAnalyser(BaseAnalyser): | |||
| Raises: | |||
| ProfilerPathErrorException: If the profiling dir is invalid. | |||
| """ | |||
| _col_names = ['op_name', 'op_type', 'execution_time', 'subgraph', | |||
| _col_names = ['op_name', 'op_type', 'avg_execution_time', 'subgraph', | |||
| 'full_op_name', 'op_info'] | |||
| _col_names_in_result = ['op_name', 'op_type', 'avg_execution_time (ms)', | |||
| 'subgraph', 'full_op_name', 'op_info'] | |||
| _file_name_aicore_detail_time = 'aicore_intermediate_{}_detail.csv' | |||
| _file_name_framework_info = 'framework_raw_{}.csv' | |||
| @@ -215,11 +231,11 @@ class AicoreDetailAnalyser(BaseAnalyser): | |||
| is_display_full_op_name (bool): Whether to display the operator full | |||
| name. | |||
| """ | |||
| self._display_col_names = self._col_names[0:4] | |||
| self._display_col_names = self._col_names_in_result[0:4] | |||
| if is_display_full_op_name: | |||
| self._display_col_names.append(self._col_names[4]) | |||
| self._display_col_names.append(self._col_names_in_result[4]) | |||
| if is_display_detail: | |||
| self._display_col_names.append(self._col_names[5]) | |||
| self._display_col_names.append(self._col_names_in_result[5]) | |||
| def _convert_framework_field_type(self, row): | |||
| """ | |||
| @@ -264,6 +280,8 @@ class AicpuAnalyser(BaseAnalyser): | |||
| """ | |||
| _col_names = ['serial_number', 'op_type', 'total_time', 'dispatch_time', | |||
| 'run_start', 'run_end'] | |||
| _col_names_in_result = ['serial_number', 'op_type', 'total_time (ms)', | |||
| 'dispatch_time (ms)', 'run_start', 'run_end'] | |||
| _file_name_aicpu_time = 'aicpu_intermediate_{}.csv' | |||
| def _load(self): | |||
| @@ -294,6 +312,16 @@ class AicpuAnalyser(BaseAnalyser): | |||
| return self._default_filter(item, filter_condition) | |||
| self._result = list(filter(_inner_filter, self._data)) | |||
| def _organize_query_result(self): | |||
| """ | |||
| Organize the query result. | |||
| Returns: | |||
| dict, the query result. | |||
| """ | |||
| self._display_col_names = self._col_names_in_result[:] | |||
| return super()._organize_query_result() | |||
| def _convert_field_type(self, row): | |||
| """ | |||
| Convert the field type to the specific type. | |||
| @@ -97,11 +97,7 @@ class BaseAnalyser(ABC): | |||
| self._sort(sort_condition) | |||
| if group_condition: | |||
| self._group(group_condition) | |||
| return { | |||
| 'col_name': self._display_col_names, | |||
| 'object': self._result, | |||
| 'size': self._size | |||
| } | |||
| return self._organize_query_result() | |||
| @abstractmethod | |||
| def _load(self): | |||
| @@ -244,3 +240,16 @@ class BaseAnalyser(ABC): | |||
| except ValidationError: | |||
| raise ProfilerPathErrorException('The profiling dir is invalid.') | |||
| return normalized_profiling_dir | |||
| def _organize_query_result(self): | |||
| """ | |||
| Organize the query result. | |||
| Returns: | |||
| dict, the query result. | |||
| """ | |||
| return { | |||
| 'col_name': self._display_col_names, | |||
| 'object': self._result, | |||
| 'size': self._size | |||
| } | |||
| @@ -116,13 +116,13 @@ class MinddataAnalyser(BaseAnalyser): | |||
| # op_info: 2: step num 3: cost time | |||
| if op_info[1] == "0": | |||
| get_time_list.append([int(op_info[2]), float(op_info[3])]) | |||
| total_cost += float(op_info[3]) | |||
| total_get += float(op_info[3]) | |||
| elif op_info[1] == "1": | |||
| push_time_list.append([int(op_info[2]), float(op_info[3])]) | |||
| total_push += float(op_info[3]) | |||
| elif op_info[1] == "2": | |||
| total_time_list.append([int(op_info[2]), float(op_info[3])]) | |||
| total_get += float(op_info[3]) | |||
| total_cost += float(op_info[3]) | |||
| elif op_info and op_info[0] == "1" and info_type in ["all", "queue"]: | |||
| queue_size_list.append([int(op_info[2]), int(op_info[3])]) | |||
| if op_info[1] == op_info[3]: | |||
| @@ -34,7 +34,6 @@ class TimelineAnalyser(BaseAnalyser): | |||
| __col_names__ = ['op_name', 'stream_id', 'start_time', 'duration'] | |||
| _output_timeline_data_file_path = 'output_timeline_data_{}.txt' | |||
| _min_cycle_counter_file_path = 'min_cycle_counter_{}.txt' | |||
| _timeline_filename = 'timeline_detail_{}.json' | |||
| _display_filename = 'timeline_display_{}.json' | |||
| _timeline_summary_filename = 'timeline_summary_{}.json' | |||
| _timeline_meta = [] | |||
| @@ -64,15 +63,9 @@ class TimelineAnalyser(BaseAnalyser): | |||
| json, the content of timeline data. | |||
| """ | |||
| # Search timeline json file under profiling dir. | |||
| timeline_filename = self._timeline_filename.format(self._device_id) | |||
| display_filename = self._display_filename.format(self._device_id) | |||
| file_list = [filename for filename in os.listdir(self._profiling_dir) | |||
| if timeline_filename in filename or display_filename in filename] | |||
| # Check if there is a timeline json file for display | |||
| file_path = os.path.join(self._profiling_dir, display_filename) | |||
| if display_filename not in file_list: | |||
| file_path = os.path.join(self._profiling_dir, timeline_filename) | |||
| file_path = validate_and_normalize_path( | |||
| file_path, raise_key='Invalid timeline json path.' | |||
| ) | |||
| @@ -121,39 +114,9 @@ class TimelineAnalyser(BaseAnalyser): | |||
| """Load data according to the parsed profiling files.""" | |||
| # Write timeline to file. | |||
| logger.info('Writing timeline file...') | |||
| file_size = self.write_timeline_to_json() | |||
| # If the file size is larger than 20MB, open a new file and | |||
| # write the first 20MB content into it. | |||
| if file_size > SIZE_LIMIT: | |||
| logger.debug('File size is larger than 20MB, will be resized...') | |||
| # write to json file for display | |||
| self.write_timeline_to_json_by_limitation() | |||
| self.write_timeline_to_json_by_limitation() | |||
| logger.info('Finished file writing!') | |||
| def write_timeline_to_json(self): | |||
| """Write timeline to json.""" | |||
| timeline_filename = self._timeline_filename.format(self._device_id) | |||
| timeline_file_path = os.path.join( | |||
| self._profiling_dir, | |||
| timeline_filename | |||
| ) | |||
| timeline_file_path = validate_and_normalize_path( | |||
| timeline_file_path, raise_key='Invalid timeline json path.' | |||
| ) | |||
| try: | |||
| with open(timeline_file_path, 'w') as json_file: | |||
| json.dump(self._timeline_meta, json_file) | |||
| file_size = os.path.getsize(timeline_file_path) | |||
| except (IOError, OSError) as err: | |||
| logger.error('Error occurred when write timeline full details: %s', err) | |||
| raise ProfilerIOException | |||
| return file_size | |||
| def write_timeline_to_json_by_limitation(self): | |||
| """Write timeline to json by limitation.""" | |||
| display_filename = self._display_filename.format(self._device_id) | |||
| @@ -161,7 +124,6 @@ class TimelineAnalyser(BaseAnalyser): | |||
| self._profiling_dir, | |||
| display_filename | |||
| ) | |||
| display_file_path = validate_and_normalize_path( | |||
| display_file_path, raise_key='Invalid timeline display json path.' | |||
| ) | |||
| @@ -224,11 +186,11 @@ class TimelineAnalyser(BaseAnalyser): | |||
| return timeline_list | |||
| def _parse_timeline_data(self, line_list): | |||
| def _parse_timeline_data(self, timeline): | |||
| """Parse timeline data.""" | |||
| # factor to convert the time unit from 1ms to 1us for timeline display | |||
| factor = 1000 | |||
| op_meta = TimelineContainer(line_list) | |||
| op_meta = TimelineContainer(timeline) | |||
| timeline_dict = {} | |||
| timeline_dict['name'] = op_meta.op_name | |||
| timeline_dict['ph'] = 'X' | |||
| @@ -245,9 +207,9 @@ class TimelineAnalyser(BaseAnalyser): | |||
| self._timeline_meta.append(timeline_dict) | |||
| @staticmethod | |||
| def _update_num_of_streams(line_list, stream_count_dict): | |||
| def _update_num_of_streams(timeline, stream_count_dict): | |||
| """Update number of streams.""" | |||
| stream_id = line_list[1] | |||
| stream_id = timeline[1] | |||
| if stream_id not in stream_count_dict.keys(): | |||
| stream_count_dict[stream_id] = 1 | |||
| else: | |||
| @@ -328,18 +290,28 @@ class TimelineAnalyser(BaseAnalyser): | |||
| framework_obj_list (list): The framework metadata. | |||
| """ | |||
| logger.debug('Start adding framework info into timeline...') | |||
| # Get the framework info that will be written into timeline. | |||
| framework_info_dict = {} | |||
| for framework_obj in framework_obj_list: | |||
| op_name = framework_obj[0] | |||
| op_type = framework_obj[1] | |||
| op_full_name = framework_obj[4] | |||
| op_info = framework_obj[5] | |||
| for timeline_obj in self._timeline_meta: | |||
| if op_full_name == timeline_obj.get('name'): | |||
| timeline_obj['name'] = op_name | |||
| timeline_obj['args'] = { | |||
| 'type': op_type, | |||
| 'fullname': op_full_name | |||
| } | |||
| timeline_obj['args'].update(op_info) | |||
| framework_info_dict[op_full_name] = { | |||
| 'name': op_name, | |||
| 'args': { | |||
| 'type': op_type, | |||
| 'fullname': op_full_name | |||
| } | |||
| } | |||
| framework_info_dict[op_full_name]['args'].update(op_info) | |||
| # Insert framework info into timeline. | |||
| for timeline_item in self._timeline_meta: | |||
| op_full_name = timeline_item.get('name') | |||
| framework_item = framework_info_dict.get(op_full_name) | |||
| if framework_item: | |||
| timeline_item['name'] = framework_item.get('name') | |||
| timeline_item['args'] = framework_item.get('args') | |||
| logger.debug('Finished adding framework info into timeline...') | |||
| @@ -118,7 +118,10 @@ def get_summary_for_step_trace(average_info, header): | |||
| def calculate_percent(partial, total): | |||
| """Calculate percent value.""" | |||
| percent = round(partial / total * 100, 2) | |||
| if total: | |||
| percent = round(partial / total * 100, 2) | |||
| else: | |||
| percent = 0 | |||
| return f'{percent}%' | |||
| @@ -24,7 +24,7 @@ from mindinsight.profiler.common.exceptions.exceptions import ProfilerParamTypeE | |||
| from mindinsight.profiler.common.log import logger as log | |||
| AICORE_TYPE_COL = ["op_type", "execution_time", "execution_frequency", "precent"] | |||
| AICORE_DETAIL_COL = ["op_name", "op_type", "execution_time", "subgraph", "full_op_name"] | |||
| AICORE_DETAIL_COL = ["op_name", "op_type", "avg_execution_time", "subgraph", "full_op_name"] | |||
| AICPU_COL = ["serial_number", "op_type", "total_time", "dispatch_time", "run_start", | |||
| "run_end"] | |||
| MINDDATA_PIPELINE_COL = [ | |||
| @@ -35,8 +35,9 @@ class DataPreProcessParser: | |||
| _source_file_target = 'DATA_PREPROCESS.dev.AICPU.' | |||
| _dst_file_title = 'title:DATA_PREPROCESS AICPU' | |||
| _dst_file_column_title = ['serial_number', 'node_type_name', 'total_time(us)', | |||
| 'dispatch_time(us)', 'run_start', 'run_end'] | |||
| _dst_file_column_title = ['serial_number', 'node_type_name', 'total_time(ms)', | |||
| 'dispatch_time(ms)', 'run_start', 'run_end'] | |||
| _ms_unit = 1000 | |||
| def __init__(self, input_path, output_filename): | |||
| self._input_path = input_path | |||
| @@ -71,8 +72,8 @@ class DataPreProcessParser: | |||
| run_start = node_list[1].split(':')[-1].split(' ')[0] | |||
| run_end = node_list[run_end_index].split(':')[-1].split(' ')[0] | |||
| total_time = thread_list[-1].split('=')[-1].split()[0] | |||
| dispatch_time = thread_list[-2].split('=')[-1].split()[0] | |||
| total_time = float(thread_list[-1].split('=')[-1].split()[0]) / self._ms_unit | |||
| dispatch_time = float(thread_list[-2].split('=')[-1].split()[0]) / self._ms_unit | |||
| return [number, node_type_name, total_time, dispatch_time, | |||
| run_start, run_end] | |||
| @@ -112,7 +113,7 @@ class DataPreProcessParser: | |||
| result_list.append(result) | |||
| # Calculate the total time. | |||
| total_time = result[2] | |||
| ai_cpu_total_time_summary += int(total_time) | |||
| ai_cpu_total_time_summary += total_time | |||
| # Increase node serial number. | |||
| serial_number += 1 | |||
| elif "Node" in node_line and "Thread" not in thread_line: | |||
| @@ -120,7 +121,8 @@ class DataPreProcessParser: | |||
| logger.warning("The node type:%s cannot find thread data", node_type_name) | |||
| if result_list: | |||
| result_list.append(["AI CPU Total Time(us):", ai_cpu_total_time_summary]) | |||
| ai_cpu_total_time = format(ai_cpu_total_time_summary, '.6f') | |||
| result_list.append(["AI CPU Total Time(ms):", ai_cpu_total_time]) | |||
| fwrite_format(self._output_filename, data_source=self._dst_file_title, is_print=True, | |||
| is_start=True) | |||
| fwrite_format(self._output_filename, | |||
| @@ -92,6 +92,7 @@ class Profiler: | |||
| os.environ['PROFILING_MODE'] = 'true' | |||
| os.environ['PROFILING_OPTIONS'] = 'training_trace:task_trace' | |||
| os.environ['MINDDATA_PROFILING_DIR'] = self._output_path | |||
| os.environ['DEVICE_ID'] = self._dev_id | |||
| # use context interface to open profiling, for the new mindspore version(after 2020.5.21) | |||
| try: | |||
| import mindspore.context as context | |||
| @@ -122,7 +122,8 @@ class PortAction(argparse.Action): | |||
| class UrlPathPrefixAction(argparse.Action): | |||
| """Url Path prefix action class definition.""" | |||
| REGEX = r'^(\/[a-zA-Z0-9-\-\.]+)+$' | |||
| INVALID_SEGMENTS = ('.', '..') | |||
| REGEX = r'^[a-zA-Z0-9_\-\.]+$' | |||
| def __call__(self, parser, namespace, values, option_string=None): | |||
| """ | |||
| @@ -135,8 +136,12 @@ class UrlPathPrefixAction(argparse.Action): | |||
| option_string (str): Optional string for specific argument name. Default: None. | |||
| """ | |||
| prefix = values | |||
| if not re.match(self.REGEX, prefix): | |||
| parser.error(f'{option_string} value is invalid url path prefix') | |||
| segments = prefix.split('/') | |||
| for index, segment in enumerate(segments): | |||
| if not segment and index in (0, len(segments) - 1): | |||
| continue | |||
| if segment in self.INVALID_SEGMENTS or not re.match(self.REGEX, segment): | |||
| parser.error(f'{option_string} value is invalid url path prefix') | |||
| setattr(namespace, self.dest, prefix) | |||
| @@ -186,7 +191,10 @@ class Command(BaseCommand): | |||
| type=str, | |||
| action=UrlPathPrefixAction, | |||
| help=""" | |||
| Custom path prefix for web page address. Default value is ''. | |||
| Custom URL path prefix for web page address. URL path prefix | |||
| consists of segments separated by slashes. Each segment supports | |||
| alphabets / digits / underscores / dashes / dots, but cannot just | |||
| be emtpy string / single dot / double dots. Default value is ''. | |||
| """) | |||
| for hook in HookUtils.instance().hooks(): | |||
| @@ -21,7 +21,7 @@ limitations under the License. | |||
| <meta charset="utf-8" /> | |||
| <meta http-equiv="X-UA-Compatible" content="IE=edge" /> | |||
| <meta name="viewport" content="width=device-width,initial-scale=1.0" /> | |||
| <link rel="icon" href="<%= BASE_URL %>/static/img/favicon.png" /> | |||
| <link rel="icon" href="static/img/favicon.png" /> | |||
| <title>MindInsight</title> | |||
| <style> | |||
| .errorInfo { | |||
| @@ -134,7 +134,7 @@ | |||
| "lessThan": "小于", | |||
| "applyAllSelectTag": "应用到当前所选标签", | |||
| "placeHolderNumber": "请输入数值", | |||
| "noSpace":"请勿输入空格", | |||
| "noSpace": "请勿输入空格", | |||
| "sameCompare": "不能有相同的比较运算符", | |||
| "unreasonable": "逻辑不合理", | |||
| "info": "提示", | |||
| @@ -248,11 +248,9 @@ | |||
| "suggestions": "优化建议", | |||
| "common-profiler_tutorial": { | |||
| "desc": "如何使用Profiler进行性能分析", | |||
| "anchor": [ | |||
| "desc" | |||
| ], | |||
| "anchor": ["desc"], | |||
| "url": [ | |||
| "https://www.mindspore.cn/tutorial/zh-CN/master/advanced_use/visualization_tutorials.html" | |||
| "https://www.mindspore.cn/tutorial/zh-CN/master/advanced_use/performance_profiling.html" | |||
| ] | |||
| }, | |||
| "step_trace-proposer_type_label": { | |||
| @@ -318,7 +316,7 @@ | |||
| "pipelineTopTitle": "算子间队列平均使用率", | |||
| "pipelineMiddleTitle": "算子间队列关系", | |||
| "deviceQueueOp": "数据发送", | |||
| "deviceQueueOpTip": "前向+后向", | |||
| "deviceQueueOpTip": "前向+反向", | |||
| "getNext": "取数据算子", | |||
| "connectorQuene": "主机队列", | |||
| "getData": "数据获取", | |||
| @@ -356,9 +354,26 @@ | |||
| "queueTip2": "队列为空比例:", | |||
| "totalCapacity": "总容量", | |||
| "averageCapacity": "平均使用容量", | |||
| "FPMessage": "FP起始算子:", | |||
| "BPMessage": "BP终止算子:", | |||
| "approximateTime": "总时长 ≈ " | |||
| "FPMessage": "前向起始算子:", | |||
| "BPMessage": "反向终止算子:", | |||
| "approximateTime": "总时长 ≈ ", | |||
| "stepInputTip": "请输入step值(1~{max}的正整数)", | |||
| "inputError": "输入参数异常,请输入一个1~{max}的正整数", | |||
| "defaultTip": "默认展示平均值", | |||
| "downloadTimeline": "下载", | |||
| "timelineTips": { | |||
| "title1": "时间线功能可以帮您对训练过程进行分析,它可以展示:", | |||
| "content11": "- 算子分配到哪个设备 (AI CPU/AI Core) 执行;", | |||
| "content12": "- MindSpore对该网络的流切分策略;", | |||
| "content13": "- 算子在Device上的执行序列和执行时长。", | |||
| "title2": "如何查看时间线:", | |||
| "content21": "要查看时间线的详细信息,您可以点击 \"下载\" 按钮将带有时间线信息的文件保存到本地,再通过工具查看。", | |||
| "content22": "我们推荐您使用谷歌插件:chrome://tracing,或 Perfetto工具:https://ui.perfetto.dev/#!/viewer", | |||
| "content23": "选择一个工具,在浏览器地址栏输入地址并回车,进入页面点击按钮加载文件查看时间线(tracing工具点击左上角 \"load\",Perfetto工具点击左侧栏 \"Open trace file\")。", | |||
| "title3": "如何使用时间线:", | |||
| "content31": "您可以通过时间线信息分析流切分方法是否合理、迭代间隙和拖尾时间是否过长等;", | |||
| "content32": "也可以具体定位到某个算子,查看分析它的执行时间。" | |||
| } | |||
| }, | |||
| "components": { | |||
| "summaryTitle": "训练选择", | |||
| @@ -16,6 +16,10 @@ | |||
| import Vue from 'vue'; | |||
| import Router from 'vue-router'; | |||
| Vue.use(Router); | |||
| const VueRouterPush = Router.prototype.push; | |||
| Router.prototype.push = function push(to) { | |||
| return VueRouterPush.call(this, to).catch((err) => err); | |||
| }; | |||
| export default new Router({ | |||
| base: process.env.BASE_URL, | |||
| @@ -77,7 +81,8 @@ export default new Router({ | |||
| children: [ | |||
| { | |||
| path: 'profiling-dashboard', | |||
| component: () => import('./views/train-manage/profiling-dashboard.vue'), | |||
| component: () => | |||
| import('./views/train-manage/profiling-dashboard.vue'), | |||
| }, | |||
| { | |||
| path: 'step-trace', | |||
| @@ -70,14 +70,15 @@ limitations under the License. | |||
| <div class="cell-container device_queue_op" | |||
| @click="highlight('device_queue_op')" | |||
| clickKey="device_queue_op" | |||
| :style="{cursor: processSummary.count !== processSummary.maxCount ? 'default' : 'pointer'}" | |||
| v-show="!processSummary.noData"> | |||
| <div class="title"> | |||
| {{$t('profiling.deviceQueueOpTip')}} | |||
| {{$t('profiling.deviceQueueOp')}} | |||
| </div> | |||
| </div> | |||
| <div class="queue-container" | |||
| v-show="processSummary.count === 6 && !processSummary.noData"> | |||
| v-show="processSummary.count === processSummary.maxCount && !processSummary.noData"> | |||
| <div class="img"> | |||
| <div class="edge"> | |||
| <img src="@/assets/images/data-flow.png" | |||
| @@ -116,7 +117,7 @@ limitations under the License. | |||
| <div class="cell-container get-next" | |||
| @click="highlight('get_next')" | |||
| clickKey="get_next" | |||
| v-if="processSummary.count === 6 && !processSummary.noData"> | |||
| v-if="processSummary.count === processSummary.maxCount && !processSummary.noData"> | |||
| <div class="title"> | |||
| {{$t('profiling.getData')}} | |||
| </div> | |||
| @@ -126,7 +127,7 @@ limitations under the License. | |||
| v-if="!(connectQueueChart.noData && dataQueueChart.noData && deviceQueueOpChart | |||
| && getNextChart.getNextChart)"> | |||
| <div class="queue-step-wrap" | |||
| v-if="processSummary.count === 6"> | |||
| v-if="processSummary.count === processSummary.maxCount"> | |||
| <div class="title">{{$t('profiling.queueStep')}}</div> | |||
| <div class="chart-content"> | |||
| <div class="chart-wrap" | |||
| @@ -164,7 +165,7 @@ limitations under the License. | |||
| </div> | |||
| </div> | |||
| <div class="queue-step-wrap" | |||
| v-if="processSummary.count === 6"> | |||
| v-if="processSummary.count === processSummary.maxCount"> | |||
| <div class="title">{{$t('profiling.operatorTimeConAnalysis')}}</div> | |||
| <div class="chart-content second"> | |||
| <div class="chart-wrap analysis" | |||
| @@ -202,7 +203,7 @@ limitations under the License. | |||
| </div> | |||
| </div> | |||
| <div class="queue-step-wrap single" | |||
| v-if="processSummary.count !== 6"> | |||
| v-if="processSummary.count !== processSummary.maxCount"> | |||
| <div class="title">{{$t('profiling.queueStep')}}</div> | |||
| <div class="chart-content"> | |||
| <div class="chart-wrap" | |||
| @@ -344,6 +345,7 @@ export default { | |||
| processSummary: { | |||
| noData: true, | |||
| count: 6, | |||
| maxCount: 6, | |||
| device: { | |||
| empty: 0, | |||
| full: 0, | |||
| @@ -530,7 +532,6 @@ export default { | |||
| xAxis: { | |||
| name: 'step', | |||
| data: [], | |||
| max: size, | |||
| }, | |||
| yAxis: {}, | |||
| series: [], | |||
| @@ -543,10 +544,13 @@ export default { | |||
| }; | |||
| option.dataZoom = [ | |||
| { | |||
| startValue: 0, | |||
| start: 0, | |||
| end: 100, | |||
| bottom: 0, | |||
| }, | |||
| { | |||
| start: 0, | |||
| end: 100, | |||
| type: 'inside', | |||
| bottom: 0, | |||
| }, | |||
| @@ -602,7 +606,7 @@ export default { | |||
| this.dealProcess(data); | |||
| this.$nextTick(() => { | |||
| if (this.processSummary.count < 6) { | |||
| if (this.processSummary.count < this.processSummary.maxCount) { | |||
| this.queryQueueInfo(this.connectQueueChart); | |||
| } else { | |||
| this.queryQueueInfo(this.connectQueueChart); | |||
| @@ -756,11 +760,10 @@ export default { | |||
| }, | |||
| ], | |||
| dataZoom: [ | |||
| {start: 0, end: 100, orient: 'vertical', right: 10}, | |||
| { | |||
| orient: 'vertical', | |||
| right: 10, | |||
| }, | |||
| { | |||
| start: 0, | |||
| end: 100, | |||
| type: 'inside', | |||
| orient: 'vertical', | |||
| right: 10, | |||
| @@ -818,7 +821,6 @@ export default { | |||
| data.sample_interval | |||
| }ms`, | |||
| data: dataY.map((val, index) => index + 1), | |||
| max: dataY.length, | |||
| }, | |||
| yAxis: { | |||
| name: '', | |||
| @@ -840,13 +842,11 @@ export default { | |||
| }, | |||
| dataZoom: [ | |||
| { | |||
| startValue: 0, | |||
| bottom: 10, | |||
| }, | |||
| { | |||
| type: 'inside', | |||
| start: 0, | |||
| end: 100, | |||
| bottom: 10, | |||
| }, | |||
| {start: 0, end: 100, type: 'inside', bottom: 10}, | |||
| ], | |||
| }; | |||
| echart.setOption(option); | |||
| @@ -855,6 +855,12 @@ export default { | |||
| }); | |||
| }, | |||
| highlight(key) { | |||
| if ( | |||
| key === 'device_queue_op' && | |||
| this.processSummary.count !== this.processSummary.maxCount | |||
| ) { | |||
| return; | |||
| } | |||
| const domList = document.querySelectorAll('.md-top *[clickKey]'); | |||
| Array.prototype.forEach.call(domList, (dom) => { | |||
| if (dom.getAttribute('clickKey') === key) { | |||
| @@ -1344,6 +1350,7 @@ export default { | |||
| .chart { | |||
| height: calc(100% - 70px); | |||
| min-height: 150px; | |||
| overflow: hidden; | |||
| } | |||
| } | |||
| .chart-wrap.highlight { | |||
| @@ -1354,15 +1361,14 @@ export default { | |||
| height: calc(100% - 25px); | |||
| } | |||
| } | |||
| .queue-step-wrap.single{ | |||
| .queue-step-wrap.single { | |||
| height: 100%; | |||
| .chart-content{ | |||
| .chart-wrap{ | |||
| .chart-content { | |||
| .chart-wrap { | |||
| width: 100%; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .pipeline-wrap { | |||
| height: 100%; | |||
| @@ -1378,6 +1384,7 @@ export default { | |||
| #average-rate { | |||
| height: 100%; | |||
| min-height: 180px; | |||
| overflow: hidden; | |||
| } | |||
| } | |||
| } | |||
| @@ -1441,6 +1448,7 @@ export default { | |||
| height: 100%; | |||
| width: 100%; | |||
| min-height: 220px; | |||
| overflow: hidden; | |||
| } | |||
| } | |||
| .right { | |||
| @@ -85,7 +85,7 @@ | |||
| :property="ele" | |||
| :key="key" | |||
| :sortable="ele === 'op_info' ? false : 'custom'" | |||
| :width="(ele==='execution_time'|| ele==='subgraph' || | |||
| :width="(ele==='avg_execution_time (ms)'|| ele==='subgraph' || | |||
| ele==='op_name'|| ele==='op_type')?'220':''" | |||
| show-overflow-tooltip | |||
| :label="ele"> | |||
| @@ -122,7 +122,7 @@ | |||
| :key="$index" | |||
| :label="item" | |||
| :sortable="item === 'op_info' ? false : 'custom'" | |||
| :width="(item==='execution_time'|| item==='subgraph' || | |||
| :width="(item==='avg_execution_time (ms)'|| item==='subgraph' || | |||
| item==='op_name'|| item==='op_type')?'220':''" | |||
| show-overflow-tooltip> | |||
| </el-table-column> | |||
| @@ -147,10 +147,9 @@ | |||
| </el-tab-pane> | |||
| <el-tab-pane label="AI CPU" | |||
| class="cpu-tab" | |||
| name="cpu" | |||
| v-if="false"> | |||
| name="cpu"> | |||
| <div class="cl-profiler-top" | |||
| v-if="cpuCharts.data.length"> | |||
| v-if="false"> | |||
| <div> | |||
| <span class="profiler-title"> | |||
| {{ $t('operator.operatorStatistics') }} | |||
| @@ -268,7 +267,7 @@ export default { | |||
| pageTotal: 0, | |||
| opDetailPage: { | |||
| offset: 0, | |||
| limit: 20, | |||
| limit: 15, | |||
| }, | |||
| op_filter_condition: {}, | |||
| op_sort_condition: { | |||
| @@ -365,7 +364,7 @@ export default { | |||
| pageTotal: 0, | |||
| opDetailPage: { | |||
| offset: 0, | |||
| limit: 20, | |||
| limit: 15, | |||
| }, | |||
| op_filter_condition: {}, | |||
| op_sort_condition: { | |||
| @@ -498,6 +497,7 @@ export default { | |||
| profile: this.profile_dir, | |||
| train_id: this.train_id, | |||
| }; | |||
| row.op_sort_condition.name = row.op_sort_condition.name.split(' ')[0]; | |||
| params.body = { | |||
| op_type: 'aicore_detail', | |||
| device_id: this.currentCard, | |||
| @@ -528,6 +528,7 @@ export default { | |||
| row.op_sort_condition.type, | |||
| ); | |||
| } | |||
| this.$refs.expandTable.doLayout(); | |||
| }); | |||
| } | |||
| }) | |||
| @@ -543,6 +544,7 @@ export default { | |||
| profile: this.profile_dir, | |||
| train_id: this.train_id, | |||
| }; | |||
| this.opCpuList.op_sort_condition.name = this.opCpuList.op_sort_condition.name.split(' ')[0]; | |||
| params.body = { | |||
| op_type: 'aicpu', | |||
| device_id: this.currentCard, | |||
| @@ -571,10 +573,6 @@ export default { | |||
| }); | |||
| this.setOption(this.cpuCharts); | |||
| } | |||
| if (res.data.object.length > 8) { | |||
| this.opCpuList.opDetailPage.limit = 8; | |||
| res.data.object.splice(8); | |||
| } | |||
| this.formatterDetailData(this.opCpuList, res.data); | |||
| if (isSort) { | |||
| this.$nextTick(() => { | |||
| @@ -625,6 +623,7 @@ export default { | |||
| } else { | |||
| this.opAllTypeList.op_filter_condition = {}; | |||
| } | |||
| this.opAllTypeList.opDetailPage.offset = 0; | |||
| this.getCoreDetailList(this.opAllTypeList, false); | |||
| } else { | |||
| this.op_filter_condition = {}; | |||
| @@ -657,6 +656,7 @@ export default { | |||
| } else { | |||
| this.opCpuList.op_filter_condition = {}; | |||
| } | |||
| this.opCpuList.opDetailPage.offset = 0; | |||
| this.getCpuList(false); | |||
| }, | |||
| /** | |||
| @@ -730,7 +730,10 @@ export default { | |||
| row.opDetailCol = []; | |||
| row.opDetailPage.offset = 0; | |||
| row.pageTotal = 0; | |||
| row.op_sort_condition = {name: 'execution_time', type: 'descending'}; | |||
| row.op_sort_condition = { | |||
| name: 'avg_execution_time', | |||
| type: 'descending', | |||
| }; | |||
| this.getCoreDetailList(row, true); | |||
| } else { | |||
| this.curActiveRow = { | |||
| @@ -769,7 +772,7 @@ export default { | |||
| coreTableChange() { | |||
| if (this.statisticType && !this.opAllTypeList.opDetailCol.length) { | |||
| this.opAllTypeList.op_sort_condition = { | |||
| name: 'execution_time', | |||
| name: 'avg_execution_time', | |||
| type: 'descending', | |||
| }; | |||
| this.getCoreDetailList(this.opAllTypeList, true); | |||
| @@ -808,7 +811,7 @@ export default { | |||
| : chart.data[i].name; | |||
| legendStr = `{a|${i + 1}}{b|${name} ${chart.data[ | |||
| i | |||
| ].value.toFixed(3)}}\n{c|${chart.data[i].percent.toFixed(2)}%}`; | |||
| ].value.toFixed(6)}}\n{c|${chart.data[i].percent.toFixed(2)}%}`; | |||
| } | |||
| } | |||
| return legendStr; | |||
| @@ -817,7 +820,7 @@ export default { | |||
| itemHeight: 18, | |||
| padding: [0, 50, 0, 0], | |||
| top: '5%', | |||
| left: '37%', | |||
| left: '45%', | |||
| textStyle: { | |||
| padding: [15, 0, 0, 0], | |||
| rich: { | |||
| @@ -849,9 +852,9 @@ export default { | |||
| option.series = [ | |||
| { | |||
| type: 'pie', | |||
| center: ['20%', '60%'], | |||
| center: ['25%', '65%'], | |||
| data: chart.data, | |||
| radius: '60%', | |||
| radius: '50%', | |||
| lable: { | |||
| position: 'outer', | |||
| alignTo: 'none', | |||
| @@ -874,9 +877,7 @@ export default { | |||
| option.tooltip = { | |||
| trigger: 'axis', | |||
| formatter: (params) => { | |||
| return `${params[0].axisValue}<br>${ | |||
| params[0].marker | |||
| }${params[0].value.toFixed(4)}`; | |||
| return `${params[0].axisValue}<br>${params[0].marker}${params[0].value}`; | |||
| }, | |||
| confine: true, | |||
| }; | |||
| @@ -1094,10 +1095,10 @@ export default { | |||
| margin-bottom: 10px; | |||
| } | |||
| .cl-profiler-top { | |||
| height: 36%; | |||
| height: 45%; | |||
| } | |||
| .cl-profiler-bottom { | |||
| height: 64%; | |||
| height: 55%; | |||
| padding-top: 10px; | |||
| } | |||
| .cpu-tab { | |||
| @@ -1105,7 +1106,7 @@ export default { | |||
| height: calc(36% + 32px); | |||
| } | |||
| .cl-profiler-bottom { | |||
| height: calc(64% - 32px); | |||
| height: 100%; | |||
| } | |||
| } | |||
| .profiler-title { | |||
| @@ -1125,7 +1126,8 @@ export default { | |||
| width: 100%; | |||
| height: 100%; | |||
| min-width: 1300px; | |||
| min-height: 232px; | |||
| min-height: 306px; | |||
| overflow: hidden; | |||
| } | |||
| } | |||
| .chart-radio-group { | |||
| @@ -186,12 +186,12 @@ limitations under the License. | |||
| <div class="cell-container device_queue_op" | |||
| clickKey="device_queue_op"> | |||
| <div class="title"> | |||
| {{$t('profiling.deviceQueueOpTip')}} | |||
| {{$t('profiling.deviceQueueOp')}} | |||
| </div> | |||
| </div> | |||
| <div class="queue-container" | |||
| v-if="processSummary.count === 6"> | |||
| v-if="processSummary.count === processSummary.maxCount"> | |||
| <div class="img"> | |||
| <div class="edge"> | |||
| <img src="@/assets/images/data-flow.png" | |||
| @@ -229,7 +229,7 @@ limitations under the License. | |||
| <div class="cell-container get-next" | |||
| clickKey="get_next" | |||
| v-if="processSummary.count === 6"> | |||
| v-if="processSummary.count === processSummary.maxCount"> | |||
| <div class="title"> | |||
| {{$t('profiling.getData')}} | |||
| </div> | |||
| @@ -292,10 +292,33 @@ limitations under the License. | |||
| <div class="title-wrap"> | |||
| <div class="title">{{ $t('profiling.timeLine') }}</div> | |||
| <div class="view-detail"> | |||
| <button @click="toPerfetto()" | |||
| :disabled="perfetto.waiting" | |||
| :class="{disabled:perfetto.waiting}">{{ $t('profiling.viewDetail') }} | |||
| <i class="el-icon-d-arrow-right"></i></button> | |||
| <button @click="downloadPerfetto()" | |||
| :disabled="timeLine.waiting" | |||
| :class="{disabled:timeLine.waiting}">{{ $t('profiling.downloadTimeline') }} | |||
| </button> | |||
| </div> | |||
| <div class="tip-icon"> | |||
| <el-tooltip placement="bottom" | |||
| effect="light"> | |||
| <div slot="content" | |||
| class="tooltip-container"> | |||
| <div class="font-size-style">{{$t("profiling.features")}}</div> | |||
| <div class="font-style">{{$t("profiling.timelineTips.title1")}}</div> | |||
| <div>{{$t("profiling.timelineTips.content11")}}</div> | |||
| <div>{{$t("profiling.timelineTips.content12")}}</div> | |||
| <div>{{$t("profiling.timelineTips.content13")}}</div> | |||
| <br> | |||
| <div class="font-style">{{$t("profiling.timelineTips.title2")}}</div> | |||
| <div>{{$t("profiling.timelineTips.content21")}}</div> | |||
| <div>{{$t("profiling.timelineTips.content22")}}</div> | |||
| <div>{{$t("profiling.timelineTips.content23")}}</div> | |||
| <br> | |||
| <div class="font-style">{{$t("profiling.timelineTips.title3")}}</div> | |||
| <div>{{$t("profiling.timelineTips.content31")}}</div> | |||
| <div>{{$t("profiling.timelineTips.content32")}}</div> | |||
| </div> | |||
| <i class="el-icon-info"></i> | |||
| </el-tooltip> | |||
| </div> | |||
| </div> | |||
| <div class="timeline-info" | |||
| @@ -351,6 +374,7 @@ export default { | |||
| totalTime: 0, | |||
| rowHeight: 60, | |||
| markerPadding: 4, | |||
| minRate: 0.05, | |||
| namespaceURI: 'http://www.w3.org/2000/svg', | |||
| resizeTimer: null, | |||
| colorList: [ | |||
| @@ -373,10 +397,8 @@ export default { | |||
| topN: [], | |||
| colorList: ['#6C92FA', '#6CBFFF', '#4EDED2', '#7ADFA0', '#A6DD82'], | |||
| }, | |||
| perfetto: { | |||
| url: 'https://ui.perfetto.dev/#!', | |||
| timeLine: { | |||
| data: null, | |||
| delay: 5000, | |||
| waiting: true, | |||
| }, | |||
| timelineInfo: { | |||
| @@ -389,6 +411,7 @@ export default { | |||
| processSummary: { | |||
| noData: true, | |||
| count: 6, | |||
| maxCount: 6, | |||
| device: { | |||
| empty: 0, | |||
| full: 0, | |||
| @@ -650,20 +673,28 @@ export default { | |||
| const traceDom = document.querySelector('#trace'); | |||
| if (traceDom) { | |||
| this.svg.totalWidth = traceDom.offsetWidth - this.svg.svgPadding * 2; | |||
| if (this.svg.data[0] && this.svg.data[0].length) { | |||
| const svg = document.querySelector('#trace svg'); | |||
| const svg = traceDom.querySelector('svg'); | |||
| this.svg.totalTime = this.svg.data[0][0].duration; | |||
| if (this.svg.totalTime) { | |||
| this.svg.colorIndex = 0; | |||
| const minTime = this.svg.minRate * this.svg.totalTime; | |||
| this.svg.data.forEach((row, index) => { | |||
| if (row && row.length) { | |||
| const dashedLine = this.addDashedLine(index); | |||
| svg.insertBefore(dashedLine, svg.querySelector('g')); | |||
| row.forEach((i) => { | |||
| if (i.duration) { | |||
| if (i.name) { | |||
| const tempDom = this.createRect(i, index); | |||
| svg.insertBefore(tempDom, svg.querySelector('g')); | |||
| const tempStr = `g${ | |||
| i.duration > minTime ? '' : '.arrow' | |||
| }`; | |||
| svg.insertBefore(tempDom, svg.querySelector(tempStr)); | |||
| } else { | |||
| const tempDom = this.createArrow(i, index); | |||
| svg.appendChild(tempDom); | |||
| @@ -690,20 +721,43 @@ export default { | |||
| line.setAttribute('style', 'stroke:#E2E2E2;stroke-width:1'); | |||
| line.setAttribute('stroke-dasharray', '5 5'); | |||
| const g = document.createElementNS(this.svg.namespaceURI, 'g'); | |||
| g.setAttribute('class', 'dashedLine'); | |||
| g.appendChild(line); | |||
| return g; | |||
| }, | |||
| createRect(data, rowIndex) { | |||
| const color = this.svg.colorList[this.svg.colorIndex++ % 4]; | |||
| const color = this.svg.colorList[ | |||
| rowIndex > 1 ? 3 : this.svg.colorIndex++ % 4 | |||
| ]; | |||
| const height = 40; | |||
| const width = (data.duration / this.svg.totalTime) * this.svg.totalWidth; | |||
| const fontSize = 12; | |||
| const normalRect = data.duration > this.svg.minRate * this.svg.totalTime; | |||
| const x1 = | |||
| (data.start / this.svg.totalTime) * this.svg.totalWidth + | |||
| this.svg.svgPadding; | |||
| const y1 = | |||
| rowIndex * this.svg.rowHeight + (this.svg.rowHeight - height) / 2; | |||
| const g = document.createElementNS(this.svg.namespaceURI, 'g'); | |||
| g.setAttribute('class', 'rect'); | |||
| const gChild = document.createElementNS(this.svg.namespaceURI, 'g'); | |||
| let name = ''; | |||
| switch (data.name) { | |||
| case 'iteration_interval': | |||
| name = this.$t('profiling.lterationGap'); | |||
| break; | |||
| case 'fp_and_bp': | |||
| name = this.$t('profiling.deviceQueueOpTip'); | |||
| break; | |||
| case 'tail': | |||
| name = this.$t('profiling.lterationTail'); | |||
| break; | |||
| default: | |||
| name = data.name; | |||
| break; | |||
| } | |||
| const rect = document.createElementNS(this.svg.namespaceURI, 'rect'); | |||
| rect.setAttribute('x', x1); | |||
| @@ -716,19 +770,33 @@ export default { | |||
| this.svg.namespaceURI, | |||
| 'foreignObject', | |||
| ); | |||
| foreignObject.setAttribute('x', x1); | |||
| foreignObject.setAttribute('y', y1); | |||
| foreignObject.setAttribute('height', height); | |||
| foreignObject.textContent = `${name}: ${data.duration.toFixed(4)}ms`; | |||
| const textWidth = this.getTextWidth(foreignObject.textContent); | |||
| foreignObject.setAttribute( | |||
| 'x', | |||
| normalRect | |||
| ? x1 | |||
| : Math.min( | |||
| this.svg.svgPadding * 2 + this.svg.totalWidth - textWidth, | |||
| Math.max(0, x1 + width / 2 - textWidth / 2), | |||
| ), | |||
| ); | |||
| foreignObject.setAttribute( | |||
| 'y', | |||
| y1 + (height - fontSize) / 2 + (normalRect ? 0 : fontSize), | |||
| ); | |||
| foreignObject.setAttribute('height', fontSize); | |||
| foreignObject.setAttribute('width', width); | |||
| foreignObject.setAttribute('style', `color:${color[0]}`); | |||
| foreignObject.setAttribute( | |||
| 'style', | |||
| `overflow:hidden;text-align:center;text-overflow:ellipsis;` + | |||
| `white-space:nowrap;font-size:12px;line-height:${height}px;color:${color[0]}`, | |||
| 'class', | |||
| `content${normalRect ? '' : ' content-mini'}`, | |||
| ); | |||
| foreignObject.textContent = `${data.name}: ${data.duration.toFixed(4)}ms`; | |||
| const title = document.createElementNS(this.svg.namespaceURI, 'title'); | |||
| title.textContent = `${data.name}: ${data.duration.toFixed(4)}ms`; | |||
| title.textContent = `${name}: ${data.duration.toFixed(4)}ms`; | |||
| gChild.appendChild(rect); | |||
| gChild.appendChild(foreignObject); | |||
| @@ -746,6 +814,7 @@ export default { | |||
| const x2 = x1 + width - this.svg.markerPadding * 2; | |||
| const y = rowIndex * this.svg.rowHeight + this.svg.rowHeight / 2; | |||
| const g = document.createElementNS(this.svg.namespaceURI, 'g'); | |||
| g.setAttribute('class', 'arrow'); | |||
| const line = document.createElementNS(this.svg.namespaceURI, 'line'); | |||
| line.setAttribute('x1', x1); | |||
| @@ -831,24 +900,6 @@ export default { | |||
| } | |||
| return new Uint8Array(arr); | |||
| }, | |||
| toPerfetto() { | |||
| if (this.perfetto.data) { | |||
| const popupwin = window.open(this.perfetto.url); | |||
| setTimeout(() => { | |||
| const params = { | |||
| perfetto: { | |||
| title: '', | |||
| buffer: this.perfetto.data, | |||
| }, | |||
| }; | |||
| if (popupwin) { | |||
| popupwin.postMessage(params, this.perfetto.url); | |||
| } | |||
| }, this.perfetto.delay); | |||
| } else { | |||
| this.perfetto.waiting = true; | |||
| } | |||
| }, | |||
| queryTimeline() { | |||
| const params = { | |||
| dir: this.relativePath, | |||
| @@ -869,18 +920,28 @@ export default { | |||
| .catch(() => { | |||
| this.timelineInfo.noData = true; | |||
| }); | |||
| this.perfetto.waiting = true; | |||
| this.timeLine.waiting = true; | |||
| RequestService.queryTimeline(params) | |||
| .then((res) => { | |||
| if (res && res.data) { | |||
| this.perfetto.data = this.stringToUint8Array( | |||
| this.timeLine.data = this.stringToUint8Array( | |||
| JSON.stringify(res.data), | |||
| ); | |||
| this.perfetto.waiting = false; | |||
| this.timeLine.waiting = false; | |||
| } | |||
| }) | |||
| .catch(() => {}); | |||
| }, | |||
| downloadPerfetto() { | |||
| const downloadLink = document.createElement('a'); | |||
| downloadLink.download = this.getDocName(); | |||
| downloadLink.style.display = 'none'; | |||
| const blob = new Blob([this.timeLine.data]); | |||
| downloadLink.href = URL.createObjectURL(blob); | |||
| document.body.appendChild(downloadLink); | |||
| downloadLink.click(); | |||
| document.body.removeChild(downloadLink); | |||
| }, | |||
| dealProcess(data) { | |||
| this.processSummary.device = { | |||
| empty: 0, | |||
| @@ -912,6 +973,22 @@ export default { | |||
| this.processSummary.noData = false; | |||
| } | |||
| }, | |||
| getDocName() { | |||
| const dealNumber = (value) => { | |||
| const prefix = value < 10 ? '0' : ''; | |||
| return prefix + value; | |||
| }; | |||
| const date = new Date(); | |||
| const year = date.getFullYear(); | |||
| const mouth = dealNumber(date.getMonth() + 1); | |||
| const day = dealNumber(date.getDate()); | |||
| const hour = dealNumber(date.getHours()); | |||
| const minute = dealNumber(date.getMinutes()); | |||
| const second = dealNumber(date.getSeconds()); | |||
| const millisecond = date.getMilliseconds(); | |||
| const timestamp = `${year}${mouth}${day}${hour}${minute}${second}${millisecond}`; | |||
| return `timeline_${this.trainingJobId}_${this.currentCard}_${timestamp}.json`; | |||
| }, | |||
| }, | |||
| destroyed() { | |||
| window.removeEventListener('resize', this.resizeTrace, false); | |||
| @@ -1021,6 +1098,17 @@ export default { | |||
| .training-trace { | |||
| position: relative; | |||
| height: 0; | |||
| .content { | |||
| overflow: hidden; | |||
| text-align: center; | |||
| text-overflow: ellipsis; | |||
| white-space: nowrap; | |||
| font-size: 12px; | |||
| line-height: 12px; | |||
| } | |||
| .content-mini { | |||
| overflow: visible; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -1198,6 +1286,7 @@ export default { | |||
| .pie-chart { | |||
| width: 100%; | |||
| height: 260px; | |||
| overflow: hidden; | |||
| } | |||
| .image-noData { | |||
| width: 100%; | |||
| @@ -952,8 +952,10 @@ export default { | |||
| } | |||
| if ( | |||
| addFlag && | |||
| parma.value[1] >= sampleObject.zoomData[0] && | |||
| parma.value[1] <= sampleObject.zoomData[1] | |||
| Math.ceil(parma.value[1] * 1000) / 1000 >= | |||
| sampleObject.zoomData[0] && | |||
| Math.floor(parma.value[1] * 1000) / 1000 <= | |||
| sampleObject.zoomData[1] | |||
| ) { | |||
| dataCount++; | |||
| runArr.push(parma.seriesName); | |||
| @@ -1674,7 +1676,7 @@ export default { | |||
| tempOption.visualMap['pieces'] && | |||
| tempOption.visualMap['pieces'].length > 0 | |||
| ) { | |||
| tempOption.visualMap = false; | |||
| tempOption.visualMap = null; | |||
| tempOption.series[0].markLine = null; | |||
| sampleObject.charObj.setOption(tempOption, true); | |||
| @@ -1810,7 +1812,7 @@ export default { | |||
| tempCharOption.visualMap['pieces'] && | |||
| tempCharOption.visualMap['pieces'].length > 0 | |||
| ) { | |||
| tempCharOption.visualMap = false; | |||
| tempCharOption.visualMap = null; | |||
| tempCharOption.series[0].markLine = null; | |||
| tempCharOption.series[0].lineStyle.color = sampleItem.colors; | |||
| } | |||
| @@ -16,21 +16,33 @@ limitations under the License. | |||
| <template> | |||
| <div class="step-trace"> | |||
| <div class="step-trace-title">{{$t('profiling.stepTraceDetail')}} | |||
| <el-tooltip class="item" | |||
| effect="light" | |||
| :content="$t('profiling.defaultTip')" | |||
| placement="top"> | |||
| <span class="el-icon-question"></span> | |||
| </el-tooltip> | |||
| <div class="pf-content-right"> | |||
| <div class="input-wrap"> | |||
| <label>{{$t('profiling.stepSelect')}}</label> | |||
| <el-select v-model="selectedStep" | |||
| filterable | |||
| :placeholder="$t('profiling.selectStep')" | |||
| @change="changeStep"> | |||
| <el-option v-for="item in steps" | |||
| :key="item.value" | |||
| :label="item.label" | |||
| :value="item.value"> | |||
| </el-option> | |||
| </el-select> | |||
| <label>{{steps.label}}</label> | |||
| <el-input ref="step" | |||
| v-model.number="steps.step" | |||
| :disabled="steps.disabled" | |||
| @blur="resetStep" | |||
| @keyup.native.enter="changeStep"> | |||
| </el-input> | |||
| <el-button @click="changeStep" | |||
| :disabled="steps.disabled"> | |||
| {{$t('public.sure')}} | |||
| </el-button> | |||
| </div> | |||
| </div> | |||
| <el-button class="show-average" | |||
| @click="changeStep(0)" | |||
| :disabled="steps.disabled"> | |||
| {{$t('profiling.showAverage')}} | |||
| </el-button> | |||
| </div> | |||
| <div class="step-message"> | |||
| <div class="step-left-padding-right"> | |||
| @@ -137,8 +149,13 @@ export default { | |||
| relativePath: this.$route.query.path, | |||
| fp_start: '--', | |||
| bp_end: '--', | |||
| steps: [], | |||
| selectedStep: '', | |||
| steps: { | |||
| step: null, | |||
| trueStep: null, | |||
| max: 0, | |||
| disabled: true, | |||
| label: this.$t('profiling.stepInputTip'), | |||
| }, | |||
| charts: [], | |||
| svg: { | |||
| data: [], | |||
| @@ -147,6 +164,7 @@ export default { | |||
| totalTime: 0, | |||
| rowHeight: 60, | |||
| markerPadding: 4, | |||
| minRate: 0.05, | |||
| namespaceURI: 'http://www.w3.org/2000/svg', | |||
| resizeTimer: null, | |||
| colorList: [ | |||
| @@ -172,7 +190,7 @@ export default { | |||
| percent: 'iteration_interval_percent', | |||
| }, | |||
| { | |||
| name: 'Fp+bp', | |||
| name: this.$t('profiling.deviceQueueOpTip'), | |||
| id: 'fp-bp', | |||
| timeSummary: {}, | |||
| rate: 'fp_and_bp', | |||
| @@ -238,17 +256,40 @@ export default { | |||
| val.clear(); | |||
| }); | |||
| } | |||
| this.setStep(); | |||
| this.steps = { | |||
| step: null, | |||
| max: 0, | |||
| disabled: true, | |||
| label: this.$t('profiling.stepInputTip'), | |||
| }; | |||
| this.getTimeInfo('fp-bp', 'fp_and_bp'); | |||
| this.getTimeInfo('iter-gap', 'iteration_interval'); | |||
| this.getTimeInfo('tailing', 'tail'); | |||
| this.queryTrainingTrace(0); | |||
| }, | |||
| changeStep(value) { | |||
| if (value === this.$t('profiling.showAverage')) { | |||
| value = 0; | |||
| if (value === 0) { | |||
| this.steps.step = null; | |||
| this.steps.trueStep = null; | |||
| this.queryTrainingTrace(0); | |||
| } else if ( | |||
| /^[0-9]*[1-9][0-9]*$/.test(this.steps.step) && | |||
| this.steps.step <= this.steps.max | |||
| ) { | |||
| this.steps.trueStep = this.steps.step; | |||
| this.queryTrainingTrace(this.steps.step); | |||
| } else { | |||
| this.steps.step = this.steps.trueStep; | |||
| this.$message.error( | |||
| this.$t('profiling.inputError').replace('{max}', this.steps.max), | |||
| ); | |||
| } | |||
| this.queryTrainingTrace(value); | |||
| }, | |||
| resetStep() { | |||
| setTimeout(() => { | |||
| this.steps.step = this.steps.trueStep; | |||
| }, 200); | |||
| }, | |||
| getTimeInfo(id, type) { | |||
| const params = { | |||
| @@ -270,8 +311,18 @@ export default { | |||
| }); | |||
| } | |||
| if (res.data && res.data.info) { | |||
| if (this.steps.length <= 1) { | |||
| this.setStep(res.data.size); | |||
| if (res.data.size && !this.steps.max) { | |||
| this.steps.max = res.data.size; | |||
| this.steps.disabled = false; | |||
| this.steps.label = this.steps.label.replace( | |||
| '{max}', | |||
| this.steps.max, | |||
| ); | |||
| } | |||
| const xAxisData = []; | |||
| for (let i = 1; i <= this.steps.max; i++) { | |||
| xAxisData.push(i); | |||
| } | |||
| const timeInfo = []; | |||
| Object.keys(res.data.info).forEach((val) => { | |||
| @@ -285,9 +336,8 @@ export default { | |||
| const option = { | |||
| xAxis: { | |||
| type: 'category', | |||
| data: this.steps.map((val, index) => index + 1), | |||
| data: xAxisData, | |||
| name: 'step', | |||
| max: this.steps.length, | |||
| }, | |||
| yAxis: { | |||
| type: 'value', | |||
| @@ -321,24 +371,23 @@ export default { | |||
| option.yAxis.name = `${this.$t( | |||
| 'profiling.iterationGapTime', | |||
| )}(ms)`; | |||
| this.tabsArr[0].noData = this.steps.length ? false : true; | |||
| this.tabsArr[0].noData = this.steps.max ? false : true; | |||
| } else if (type === 'fp_and_bp') { | |||
| option.yAxis.name = `fp+bp${this.$t('profiling.time')}(ms)`; | |||
| this.tabsArr[1].noData = this.steps.length ? false : true; | |||
| option.yAxis.name = `${this.$t( | |||
| 'profiling.deviceQueueOpTip', | |||
| )}${this.$t('profiling.time')}(ms)`; | |||
| this.tabsArr[1].noData = this.steps.max ? false : true; | |||
| } else if (type === 'tail') { | |||
| option.yAxis.name = `tail${this.$t('profiling.time')}(ms)`; | |||
| this.tabsArr[2].noData = this.steps.length ? false : true; | |||
| option.yAxis.name = `${this.$t( | |||
| 'profiling.lterationTail', | |||
| )}${this.$t('profiling.time')}(ms)`; | |||
| this.tabsArr[2].noData = this.steps.max ? false : true; | |||
| } | |||
| this.initChart(option, id); | |||
| } else { | |||
| this.steps = []; | |||
| this.selectedStep = ''; | |||
| } | |||
| } | |||
| }, | |||
| (error) => { | |||
| this.steps = []; | |||
| this.selectedStep = ''; | |||
| if (type === 'iteration_interval') { | |||
| this.tabsArr[0].noData = true; | |||
| } else if (type === 'fp_and_bp') { | |||
| @@ -349,20 +398,6 @@ export default { | |||
| }, | |||
| ); | |||
| }, | |||
| setStep(step = 0) { | |||
| this.steps = []; | |||
| this.steps.push({ | |||
| label: this.$t('profiling.showAverage'), | |||
| value: this.$t('profiling.showAverage'), | |||
| }); | |||
| for (let i = 1; i <= step; i++) { | |||
| this.steps.push({ | |||
| label: i, | |||
| value: i, | |||
| }); | |||
| } | |||
| this.selectedStep = this.$t('profiling.showAverage'); | |||
| }, | |||
| initChart(option, id) { | |||
| this.$nextTick(() => { | |||
| const chart = echarts.init(document.getElementById(id)); | |||
| @@ -430,33 +465,43 @@ export default { | |||
| }, | |||
| dealTraceData() { | |||
| this.svg.totalWidth = | |||
| document.querySelector('#trace').offsetWidth - this.svg.svgPadding * 2; | |||
| if (this.svg.data[0] && this.svg.data[0].length) { | |||
| const svg = document.querySelector('#trace svg'); | |||
| this.svg.totalTime = this.svg.data[0][0].duration; | |||
| if (this.svg.totalTime) { | |||
| this.svg.colorIndex = 0; | |||
| this.svg.data.forEach((row, index) => { | |||
| if (row && row.length) { | |||
| const dashedLine = this.addDashedLine(index); | |||
| svg.insertBefore(dashedLine, svg.querySelector('g')); | |||
| row.forEach((i) => { | |||
| if (i.duration) { | |||
| if (i.name) { | |||
| const tempDom = this.createRect(i, index); | |||
| svg.insertBefore(tempDom, svg.querySelector('g')); | |||
| } else { | |||
| const tempDom = this.createArrow(i, index); | |||
| svg.appendChild(tempDom); | |||
| const traceDom = document.querySelector('#trace'); | |||
| if (traceDom) { | |||
| this.svg.totalWidth = traceDom.offsetWidth - this.svg.svgPadding * 2; | |||
| if (this.svg.data[0] && this.svg.data[0].length) { | |||
| const svg = traceDom.querySelector('svg'); | |||
| this.svg.totalTime = this.svg.data[0][0].duration; | |||
| if (this.svg.totalTime) { | |||
| this.svg.colorIndex = 0; | |||
| const minTime = this.svg.minRate * this.svg.totalTime; | |||
| this.svg.data.forEach((row, index) => { | |||
| if (row && row.length) { | |||
| const dashedLine = this.addDashedLine(index); | |||
| svg.insertBefore(dashedLine, svg.querySelector('g')); | |||
| row.forEach((i) => { | |||
| if (i.duration) { | |||
| if (i.name) { | |||
| const tempDom = this.createRect(i, index); | |||
| const tempStr = `g${ | |||
| i.duration > minTime ? '' : '.arrow' | |||
| }`; | |||
| svg.insertBefore(tempDom, svg.querySelector(tempStr)); | |||
| } else { | |||
| const tempDom = this.createArrow(i, index); | |||
| svg.appendChild(tempDom); | |||
| } | |||
| } | |||
| } | |||
| }); | |||
| } | |||
| }); | |||
| }); | |||
| } | |||
| }); | |||
| } | |||
| } else { | |||
| this.removeTrace(); | |||
| } | |||
| } else { | |||
| this.removeTrace(); | |||
| } | |||
| }, | |||
| addDashedLine(index) { | |||
| @@ -471,20 +516,43 @@ export default { | |||
| line.setAttribute('style', 'stroke:#E2E2E2;stroke-width:1'); | |||
| line.setAttribute('stroke-dasharray', '5 5'); | |||
| const g = document.createElementNS(this.svg.namespaceURI, 'g'); | |||
| g.setAttribute('class', 'dashedLine'); | |||
| g.appendChild(line); | |||
| return g; | |||
| }, | |||
| createRect(data, rowIndex) { | |||
| const color = this.svg.colorList[this.svg.colorIndex++ % 4]; | |||
| const color = this.svg.colorList[ | |||
| rowIndex > 1 ? 3 : this.svg.colorIndex++ % 4 | |||
| ]; | |||
| const height = 40; | |||
| const width = (data.duration / this.svg.totalTime) * this.svg.totalWidth; | |||
| const fontSize = 12; | |||
| const normalRect = data.duration > this.svg.minRate * this.svg.totalTime; | |||
| const x1 = | |||
| (data.start / this.svg.totalTime) * this.svg.totalWidth + | |||
| this.svg.svgPadding; | |||
| const y1 = | |||
| rowIndex * this.svg.rowHeight + (this.svg.rowHeight - height) / 2; | |||
| const g = document.createElementNS(this.svg.namespaceURI, 'g'); | |||
| g.setAttribute('class', 'rect'); | |||
| const gChild = document.createElementNS(this.svg.namespaceURI, 'g'); | |||
| let name = ''; | |||
| switch (data.name) { | |||
| case 'iteration_interval': | |||
| name = this.$t('profiling.lterationGap'); | |||
| break; | |||
| case 'fp_and_bp': | |||
| name = this.$t('profiling.deviceQueueOpTip'); | |||
| break; | |||
| case 'tail': | |||
| name = this.$t('profiling.lterationTail'); | |||
| break; | |||
| default: | |||
| name = data.name; | |||
| break; | |||
| } | |||
| const rect = document.createElementNS(this.svg.namespaceURI, 'rect'); | |||
| rect.setAttribute('x', x1); | |||
| @@ -497,19 +565,33 @@ export default { | |||
| this.svg.namespaceURI, | |||
| 'foreignObject', | |||
| ); | |||
| foreignObject.setAttribute('x', x1); | |||
| foreignObject.setAttribute('y', y1); | |||
| foreignObject.setAttribute('height', height); | |||
| foreignObject.textContent = `${name}: ${data.duration.toFixed(4)}ms`; | |||
| const textWidth = this.getTextWidth(foreignObject.textContent); | |||
| foreignObject.setAttribute( | |||
| 'x', | |||
| normalRect | |||
| ? x1 | |||
| : Math.min( | |||
| this.svg.svgPadding * 2 + this.svg.totalWidth - textWidth, | |||
| Math.max(0, x1 + width / 2 - textWidth / 2), | |||
| ), | |||
| ); | |||
| foreignObject.setAttribute( | |||
| 'y', | |||
| y1 + (height - fontSize) / 2 + (normalRect ? 0 : fontSize), | |||
| ); | |||
| foreignObject.setAttribute('height', fontSize); | |||
| foreignObject.setAttribute('width', width); | |||
| foreignObject.setAttribute('style', `color:${color[0]}`); | |||
| foreignObject.setAttribute( | |||
| 'style', | |||
| `overflow:hidden;text-align:center;text-overflow:ellipsis;` + | |||
| `white-space:nowrap;font-size:12px;line-height:${height}px;color:${color[0]}`, | |||
| 'class', | |||
| `content${normalRect ? '' : ' content-mini'}`, | |||
| ); | |||
| foreignObject.textContent = `${data.name}: ${data.duration.toFixed(4)}ms`; | |||
| const title = document.createElementNS(this.svg.namespaceURI, 'title'); | |||
| title.textContent = `${data.name}: ${data.duration.toFixed(4)}ms`; | |||
| title.textContent = `${name}: ${data.duration.toFixed(4)}ms`; | |||
| gChild.appendChild(rect); | |||
| gChild.appendChild(foreignObject); | |||
| @@ -527,6 +609,7 @@ export default { | |||
| const x2 = x1 + width - this.svg.markerPadding * 2; | |||
| const y = rowIndex * this.svg.rowHeight + this.svg.rowHeight / 2; | |||
| const g = document.createElementNS(this.svg.namespaceURI, 'g'); | |||
| g.setAttribute('class', 'arrow'); | |||
| const line = document.createElementNS(this.svg.namespaceURI, 'line'); | |||
| line.setAttribute('x1', x1); | |||
| @@ -630,15 +713,36 @@ export default { | |||
| padding: 0 15px; | |||
| font-size: 16px; | |||
| font-weight: bold; | |||
| .el-icon-question { | |||
| cursor: pointer; | |||
| } | |||
| .pf-content-right { | |||
| display: inline-block; | |||
| margin-left: 35px; | |||
| label { | |||
| margin-right: 10px; | |||
| .input-wrap { | |||
| font-weight: normal; | |||
| label { | |||
| margin-right: 20px; | |||
| } | |||
| .el-input { | |||
| width: 150px; | |||
| margin-right: 16px; | |||
| } | |||
| } | |||
| } | |||
| .el-button { | |||
| border: 1px solid #00a5a7; | |||
| border-radius: 2px; | |||
| background-color: white; | |||
| color: #00a5a7; | |||
| padding: 7px 15px; | |||
| &:hover { | |||
| background: rgb(230, 246, 246); | |||
| } | |||
| } | |||
| .input-wrap { | |||
| font-weight: normal; | |||
| .show-average { | |||
| float: right; | |||
| margin-right: 20px; | |||
| } | |||
| } | |||
| .step-message { | |||
| @@ -680,6 +784,17 @@ export default { | |||
| cursor: pointer; | |||
| background-image: url('../../assets/images/download.png'); | |||
| } | |||
| .content { | |||
| overflow: hidden; | |||
| text-align: center; | |||
| text-overflow: ellipsis; | |||
| white-space: nowrap; | |||
| font-size: 12px; | |||
| line-height: 12px; | |||
| } | |||
| .content-mini { | |||
| overflow: visible; | |||
| } | |||
| } | |||
| } | |||
| .chart-wrap { | |||
| @@ -698,6 +813,7 @@ export default { | |||
| .chart { | |||
| height: calc(100% - 85px); | |||
| min-height: 180px; | |||
| overflow: hidden; | |||
| } | |||
| .title { | |||
| margin: 0 0 15px 20px; | |||
| @@ -70,6 +70,7 @@ class DataVisualErrors(Enum): | |||
| SCALAR_NOT_EXIST = 14 | |||
| HISTOGRAM_NOT_EXIST = 15 | |||
| TRAIN_JOB_DETAIL_NOT_IN_CACHE = 16 | |||
| QUERY_STRING_CONTAINS_NULL_BYTE = 17 | |||
| class ScriptConverterErrors(Enum): | |||
| @@ -79,9 +79,9 @@ class TestPlugins: | |||
| @pytest.mark.platform_x86_gpu_training | |||
| @pytest.mark.platform_x86_ascend_training | |||
| @pytest.mark.usefixtures("init_summary_logs") | |||
| @pytest.mark.parametrize("train_id", ["@#$", "./\x00home", "././/not_exist_id", dict()]) | |||
| @pytest.mark.parametrize("train_id", ["@#$", "././/not_exist_id", dict()]) | |||
| def test_plugins_with_special_train_id(self, client, train_id): | |||
| """Test passing train_id with special character, null_byte, invalid id, and wrong type.""" | |||
| """Test passing train_id with special character, invalid id, and wrong type.""" | |||
| params = dict(train_id=train_id) | |||
| url = get_url(BASE_URL, params) | |||
| @@ -92,6 +92,26 @@ class TestPlugins: | |||
| assert response['error_code'] == '50545005' | |||
| assert response['error_msg'] == "Train job is not exist. Detail: Can not find the train job in data manager." | |||
| @pytest.mark.level1 | |||
| @pytest.mark.env_single | |||
| @pytest.mark.platform_x86_cpu | |||
| @pytest.mark.platform_arm_ascend_training | |||
| @pytest.mark.platform_x86_gpu_training | |||
| @pytest.mark.platform_x86_ascend_training | |||
| @pytest.mark.usefixtures("init_summary_logs") | |||
| @pytest.mark.parametrize("train_id", ["./\x00home"]) | |||
| def test_plugins_with_null_byte_train_id(self, client, train_id): | |||
| """Test passing train_id with null_byte.""" | |||
| params = dict(train_id=train_id, manual_update=True) | |||
| url = get_url(BASE_URL, params) | |||
| response = client.get(url) | |||
| assert response.status_code == 400 | |||
| response = response.get_json() | |||
| assert response['error_code'] == '50545011' | |||
| assert "Query string contains null byte error. " in response['error_msg'] | |||
| @pytest.mark.level1 | |||
| @pytest.mark.env_single | |||
| @pytest.mark.platform_x86_cpu | |||
| @@ -32,7 +32,8 @@ from tests.ut.profiler import RAW_DATA_BASE | |||
| OP_GATHER_V2_INFO = { | |||
| 'col_name': [ | |||
| 'op_name', 'op_type', 'execution_time', 'subgraph', 'full_op_name', 'op_info' | |||
| 'op_name', 'op_type', 'avg_execution_time (ms)', 'subgraph', 'full_op_name', | |||
| 'op_info' | |||
| ], | |||
| 'object': [ | |||
| [ | |||
| @@ -124,36 +125,36 @@ class TestOpAnalyser: | |||
| def test_query_aicore_type_1(self): | |||
| """Test the function of querying AICORE operator type infomation.""" | |||
| expect_result = { | |||
| 'col_name': ['op_type', 'execution_time', 'execution_frequency', 'percent'], | |||
| 'col_name': ['op_type', 'execution_time (ms)', 'execution_frequency', 'percent'], | |||
| 'object': [ | |||
| ['UnsortedSegmentSum', 44.60782642857142, 2, 35.28], | |||
| ['GatherV2', 43.15544147619047, 2, 34.13], | |||
| ['Slice', 20.376314999999998, 16, 16.12], | |||
| ['Concat', 5.80845380952381, 4, 4.59], | |||
| ['Split', 2.7142774761904764, 2, 2.15], | |||
| ['MatMul', 1.9366814285714287, 15, 1.53], | |||
| ['Mul', 1.9029486666666666, 32, 1.51], | |||
| ['StridedSliceGrad', 1.5068342857142858, 2, 1.19], | |||
| ['TransData', 1.1151575238095237, 30, 0.88], | |||
| ['ReluGrad', 0.8540685714285714, 5, 0.68], | |||
| ['Cast', 0.4846848571428572, 15, 0.38], | |||
| ['ReLU', 0.48328214285714277, 5, 0.38], | |||
| ['RealDiv', 0.4228071904761905, 15, 0.33], | |||
| ['StridedSlice', 0.3455687619047618, 2, 0.27], | |||
| ['Adam', 0.2859357142857143, 11, 0.23], | |||
| ['BiasAdd', 0.18966285714285713, 5, 0.15], | |||
| ['BiasAddGrad', 0.07168142857142856, 5, 0.06], | |||
| ['Tile', 0.04415833333333334, 4, 0.03], | |||
| ['ReduceSum', 0.030764857142857142, 5, 0.02], | |||
| ['ApplyFtrl', 0.025453571428571426, 2, 0.02], | |||
| ['AtomicAddrClean', 0.019368666666666666, 8, 0.02], | |||
| ['AddN', 0.012836428571428572, 1, 0.01], | |||
| ['Square', 0.009799333333333334, 1, 0.01], | |||
| ['SigmoidCrossEntropyWithLogitsGrad', 0.009582142857142859, 2, 0.01], | |||
| ['TensorAdd', 0.009218380952380952, 3, 0.01], | |||
| ['SigmoidCrossEntropyWithLogits', 0.004808571428571428, 1, 0.0], | |||
| ['ReduceMean', 0.004534999999999999, 1, 0.0], | |||
| ['Assign', 0.0024766666666666665, 2, 0.0], | |||
| ['UnsortedSegmentSum', 44.607826, 2, 35.28], | |||
| ['GatherV2', 43.155441, 2, 34.13], | |||
| ['Slice', 20.376315, 16, 16.12], | |||
| ['Concat', 5.808454, 4, 4.59], | |||
| ['Split', 2.714277, 2, 2.15], | |||
| ['MatMul', 1.936681, 15, 1.53], | |||
| ['Mul', 1.902949, 32, 1.51], | |||
| ['StridedSliceGrad', 1.506834, 2, 1.19], | |||
| ['TransData', 1.115158, 30, 0.88], | |||
| ['ReluGrad', 0.854069, 5, 0.68], | |||
| ['Cast', 0.484685, 15, 0.38], | |||
| ['ReLU', 0.483282, 5, 0.38], | |||
| ['RealDiv', 0.422807, 15, 0.33], | |||
| ['StridedSlice', 0.345569, 2, 0.27], | |||
| ['Adam', 0.285936, 11, 0.23], | |||
| ['BiasAdd', 0.189663, 5, 0.15], | |||
| ['BiasAddGrad', 0.071681, 5, 0.06], | |||
| ['Tile', 0.044158, 4, 0.03], | |||
| ['ReduceSum', 0.030765, 5, 0.02], | |||
| ['ApplyFtrl', 0.025454, 2, 0.02], | |||
| ['AtomicAddrClean', 0.019369, 8, 0.02], | |||
| ['AddN', 0.012836, 1, 0.01], | |||
| ['Square', 0.009799, 1, 0.01], | |||
| ['SigmoidCrossEntropyWithLogitsGrad', 0.009582, 2, 0.01], | |||
| ['TensorAdd', 0.009218, 3, 0.01], | |||
| ['SigmoidCrossEntropyWithLogits', 0.004809, 1, 0.0], | |||
| ['ReduceMean', 0.004535, 1, 0.0], | |||
| ['Assign', 0.002477, 2, 0.0], | |||
| ['AssignAdd', 0.001688, 1, 0.0] | |||
| ], | |||
| 'size': 29 | |||
| @@ -176,10 +177,10 @@ class TestOpAnalyser: | |||
| def test_query_aicore_type_2(self): | |||
| """Test the function of querying AICORE operator type infomation.""" | |||
| expect_result = { | |||
| 'col_name': ['op_type', 'execution_time', 'execution_frequency', 'percent'], | |||
| 'col_name': ['op_type', 'execution_time (ms)', 'execution_frequency', 'percent'], | |||
| 'object': [ | |||
| ['MatMul', 1.9366814285714287, 15, 1.53], | |||
| ['Mul', 1.9029486666666666, 32, 1.51] | |||
| ['MatMul', 1.936681, 15, 1.53], | |||
| ['Mul', 1.902949, 32, 1.51] | |||
| ], | |||
| 'size': 2 | |||
| } | |||
| @@ -213,7 +214,7 @@ class TestOpAnalyser: | |||
| } | |||
| }, | |||
| 'sort_condition': { | |||
| 'name': 'execution_time', | |||
| 'name': 'avg_execution_time', | |||
| 'type': 'descending' | |||
| }, | |||
| 'group_condition': { | |||
| @@ -21,8 +21,10 @@ from unittest import TestCase | |||
| from mindinsight.profiler.analyser.analyser_factory import AnalyserFactory | |||
| from tests.ut.profiler import PROFILER_DIR | |||
| COL_NAMES = ['op_name', 'op_type', 'execution_time', 'subgraph', 'full_op_name', | |||
| 'op_info'] | |||
| COL_NAMES = ['op_name', 'op_type', 'avg_execution_time', 'subgraph', | |||
| 'full_op_name', 'op_info'] | |||
| COL_NAMES_IN_RESULT = ['op_name', 'op_type', 'avg_execution_time (ms)', | |||
| 'subgraph', 'full_op_name', 'op_info'] | |||
| def get_detail_infos(indexes=None, sort_name=None, sort_type=True): | |||
| @@ -75,7 +77,7 @@ class TestAicoreDetailAnalyser(TestCase): | |||
| def test_query_success_1(self): | |||
| """Test the success of the querying function.""" | |||
| expect_result = { | |||
| 'col_name': COL_NAMES, | |||
| 'col_name': COL_NAMES_IN_RESULT, | |||
| 'object': get_detail_infos(), | |||
| 'size': 10 | |||
| } | |||
| @@ -88,7 +90,7 @@ class TestAicoreDetailAnalyser(TestCase): | |||
| def test_query_success_2(self): | |||
| """Test the success of the querying function.""" | |||
| expect_result = { | |||
| 'col_name': COL_NAMES, | |||
| 'col_name': COL_NAMES_IN_RESULT, | |||
| 'object': get_detail_infos(indexes=[9]), | |||
| 'size': 1 | |||
| } | |||
| @@ -125,13 +127,13 @@ class TestAicoreDetailAnalyser(TestCase): | |||
| def test_query_success_3(self): | |||
| """Test the success of the querying function.""" | |||
| expect_result = { | |||
| 'col_name': COL_NAMES, | |||
| 'object': get_detail_infos(sort_name='execution_time', sort_type=True), | |||
| 'col_name': COL_NAMES_IN_RESULT, | |||
| 'object': get_detail_infos(sort_name='avg_execution_time', sort_type=True), | |||
| 'size': 10 | |||
| } | |||
| condition = { | |||
| 'sort_condition': { | |||
| 'name': 'execution_time', | |||
| 'name': 'avg_execution_time', | |||
| 'type': 'descending' | |||
| } | |||
| } | |||
| @@ -139,7 +141,7 @@ class TestAicoreDetailAnalyser(TestCase): | |||
| self.assertDictEqual(expect_result, result) | |||
| expect_result = { | |||
| 'col_name': COL_NAMES, | |||
| 'col_name': COL_NAMES_IN_RESULT, | |||
| 'object': get_detail_infos(sort_name='op_name', sort_type=False), | |||
| 'size': 10 | |||
| } | |||
| @@ -155,7 +157,7 @@ class TestAicoreDetailAnalyser(TestCase): | |||
| def test_query_success_4(self): | |||
| """Test the success of the querying function.""" | |||
| expect_result = { | |||
| 'col_name': COL_NAMES, | |||
| 'col_name': COL_NAMES_IN_RESULT, | |||
| 'object': get_detail_infos(indexes=[2, 3]), | |||
| 'size': 10 | |||
| } | |||
| @@ -169,7 +171,7 @@ class TestAicoreDetailAnalyser(TestCase): | |||
| self.assertDictEqual(expect_result, result) | |||
| expect_result = { | |||
| 'col_name': COL_NAMES, | |||
| 'col_name': COL_NAMES_IN_RESULT, | |||
| 'object': [], | |||
| 'size': 10 | |||
| } | |||
| @@ -185,9 +187,9 @@ class TestAicoreDetailAnalyser(TestCase): | |||
| def test_query_success_5(self): | |||
| """Test the success of the querying function.""" | |||
| expect_result = { | |||
| 'col_name': COL_NAMES, | |||
| 'col_name': COL_NAMES_IN_RESULT, | |||
| 'object': get_detail_infos( | |||
| indexes=[1, 2], sort_name='execution_time', sort_type=True | |||
| indexes=[1, 2], sort_name='avg_execution_time', sort_type=True | |||
| ), | |||
| 'size': 4 | |||
| } | |||
| @@ -198,7 +200,7 @@ class TestAicoreDetailAnalyser(TestCase): | |||
| } | |||
| }, | |||
| 'sort_condition': { | |||
| 'name': 'execution_time' | |||
| 'name': 'avg_execution_time' | |||
| }, | |||
| 'group_condition': { | |||
| 'limit': 2, | |||
| @@ -209,9 +211,9 @@ class TestAicoreDetailAnalyser(TestCase): | |||
| self.assertDictEqual(expect_result, result) | |||
| expect_result = { | |||
| 'col_name': COL_NAMES, | |||
| 'col_name': COL_NAMES_IN_RESULT, | |||
| 'object': get_detail_infos( | |||
| indexes=[0, 1, 2, 8], sort_name='execution_time', sort_type=True | |||
| indexes=[0, 1, 2, 8], sort_name='avg_execution_time', sort_type=True | |||
| ), | |||
| 'size': 4 | |||
| } | |||
| @@ -225,7 +227,7 @@ class TestAicoreDetailAnalyser(TestCase): | |||
| } | |||
| }, | |||
| 'sort_condition': { | |||
| 'name': 'execution_time' | |||
| 'name': 'avg_execution_time' | |||
| } | |||
| } | |||
| result = self._analyser.query(condition) | |||
| @@ -236,7 +238,7 @@ class TestAicoreDetailAnalyser(TestCase): | |||
| detail_infos = get_detail_infos(indexes=[9]) | |||
| expect_result = { | |||
| 'col_name': COL_NAMES[0:5], | |||
| 'col_name': COL_NAMES_IN_RESULT[0:5], | |||
| 'object': [item[0:5] for item in detail_infos], | |||
| 'size': 1 | |||
| } | |||
| @@ -252,7 +254,7 @@ class TestAicoreDetailAnalyser(TestCase): | |||
| self.assertDictEqual(expect_result, result) | |||
| expect_result = { | |||
| 'col_name': COL_NAMES[0:4], | |||
| 'col_name': COL_NAMES_IN_RESULT[0:4], | |||
| 'object': [item[0:4] for item in detail_infos], | |||
| 'size': 1 | |||
| } | |||
| @@ -272,7 +274,7 @@ class TestAicoreDetailAnalyser(TestCase): | |||
| """Test the success of the querying and sorting function by operator type.""" | |||
| detail_infos = get_detail_infos(indexes=[9, 0, 2, 1, 5, 3, 4]) | |||
| expect_result = { | |||
| 'col_name': COL_NAMES[0:4], | |||
| 'col_name': COL_NAMES_IN_RESULT[0:4], | |||
| 'object': [item[0:4] for item in detail_infos] | |||
| } | |||
| @@ -294,7 +296,7 @@ class TestAicoreDetailAnalyser(TestCase): | |||
| """Test the success of the querying and sorting function by operator type.""" | |||
| detail_infos = get_detail_infos(indexes=[9, 0, 2, 1, 3, 4, 8, 6]) | |||
| expect_result = { | |||
| 'col_name': COL_NAMES[0:4], | |||
| 'col_name': COL_NAMES_IN_RESULT[0:4], | |||
| 'object': [item[0:4] for item in detail_infos] | |||
| } | |||
| @@ -21,6 +21,8 @@ from mindinsight.profiler.analyser.analyser_factory import AnalyserFactory | |||
| from tests.ut.profiler import PROFILER_DIR | |||
| COL_NAMES = ['op_type', 'execution_time', 'execution_frequency', 'percent'] | |||
| COL_NAMES_IN_RESULT = ['op_type', 'execution_time (ms)', 'execution_frequency', | |||
| 'percent'] | |||
| def get_type_infos(indexes=None, sort_name=None, sort_type=True): | |||
| @@ -72,7 +74,7 @@ class TestAicoreTypeAnalyser(TestCase): | |||
| def test_query_success_1(self): | |||
| """Test the success of the querying function.""" | |||
| expect_result = { | |||
| 'col_name': COL_NAMES, | |||
| 'col_name': COL_NAMES_IN_RESULT, | |||
| 'object': get_type_infos(), | |||
| 'size': 5 | |||
| } | |||
| @@ -86,7 +88,7 @@ class TestAicoreTypeAnalyser(TestCase): | |||
| def test_query_success_2(self): | |||
| """Test the success of the querying function.""" | |||
| expect_result = { | |||
| 'col_name': COL_NAMES, | |||
| 'col_name': COL_NAMES_IN_RESULT, | |||
| 'object': get_type_infos(indexes=[1]), | |||
| 'size': 1 | |||
| } | |||
| @@ -101,7 +103,7 @@ class TestAicoreTypeAnalyser(TestCase): | |||
| self.assertDictEqual(expect_result, result) | |||
| expect_result = { | |||
| 'col_name': COL_NAMES, | |||
| 'col_name': COL_NAMES_IN_RESULT, | |||
| 'object': get_type_infos(indexes=[0, 2, 3, 4]), | |||
| 'size': 4 | |||
| } | |||
| @@ -116,7 +118,7 @@ class TestAicoreTypeAnalyser(TestCase): | |||
| self.assertDictEqual(expect_result, result) | |||
| expect_result = { | |||
| 'col_name': COL_NAMES, | |||
| 'col_name': COL_NAMES_IN_RESULT, | |||
| 'object': get_type_infos(indexes=[0, 1, 3]), | |||
| 'size': 3 | |||
| } | |||
| @@ -133,7 +135,7 @@ class TestAicoreTypeAnalyser(TestCase): | |||
| def test_query_success_3(self): | |||
| """Test the success of the querying function.""" | |||
| expect_result = { | |||
| 'col_name': COL_NAMES, | |||
| 'col_name': COL_NAMES_IN_RESULT, | |||
| 'object': get_type_infos(indexes=[1, 3]), | |||
| 'size': 2 | |||
| } | |||
| @@ -148,7 +150,7 @@ class TestAicoreTypeAnalyser(TestCase): | |||
| self.assertDictEqual(expect_result, result) | |||
| expect_result = { | |||
| 'col_name': COL_NAMES, | |||
| 'col_name': COL_NAMES_IN_RESULT, | |||
| 'object': get_type_infos(indexes=[0, 2, 4]), | |||
| 'size': 3 | |||
| } | |||
| @@ -163,7 +165,7 @@ class TestAicoreTypeAnalyser(TestCase): | |||
| self.assertDictEqual(expect_result, result) | |||
| expect_result = { | |||
| 'col_name': COL_NAMES, | |||
| 'col_name': COL_NAMES_IN_RESULT, | |||
| 'object': get_type_infos(indexes=[2, 3]), | |||
| 'size': 2 | |||
| } | |||
| @@ -180,7 +182,7 @@ class TestAicoreTypeAnalyser(TestCase): | |||
| def test_query_success_4(self): | |||
| """Test the success of the querying function.""" | |||
| expect_result = { | |||
| 'col_name': COL_NAMES, | |||
| 'col_name': COL_NAMES_IN_RESULT, | |||
| 'object': get_type_infos(sort_name='op_type', sort_type=True), | |||
| 'size': 5} | |||
| condition = { | |||
| @@ -193,7 +195,7 @@ class TestAicoreTypeAnalyser(TestCase): | |||
| self.assertDictEqual(expect_result, result) | |||
| expect_result = { | |||
| 'col_name': COL_NAMES, | |||
| 'col_name': COL_NAMES_IN_RESULT, | |||
| 'object': get_type_infos(sort_name='execution_time', sort_type=False), | |||
| 'size': 5 | |||
| } | |||
| @@ -209,7 +211,7 @@ class TestAicoreTypeAnalyser(TestCase): | |||
| def test_query_success_5(self): | |||
| """Test the success of the querying function.""" | |||
| expect_result = { | |||
| 'col_name': COL_NAMES, | |||
| 'col_name': COL_NAMES_IN_RESULT, | |||
| 'object': get_type_infos(indexes=[0, 1]), | |||
| 'size': 5 | |||
| } | |||
| @@ -223,7 +225,7 @@ class TestAicoreTypeAnalyser(TestCase): | |||
| self.assertDictEqual(expect_result, result) | |||
| expect_result = { | |||
| 'col_name': COL_NAMES, | |||
| 'col_name': COL_NAMES_IN_RESULT, | |||
| 'object': get_type_infos(indexes=[3, 4]), | |||
| 'size': 5 | |||
| } | |||
| @@ -239,7 +241,7 @@ class TestAicoreTypeAnalyser(TestCase): | |||
| def test_query_success_6(self): | |||
| """Test the success of the querying function.""" | |||
| expect_result = { | |||
| 'col_name': COL_NAMES, | |||
| 'col_name': COL_NAMES_IN_RESULT, | |||
| 'object': get_type_infos( | |||
| indexes=[1, 3], sort_name='execution_time', sort_type=True | |||
| ), | |||
| @@ -1,6 +1,6 @@ | |||
| serial_number node_type_name total_time(us) dispatch_time(us) run_start run_end | |||
| serial_number node_type_name total_time(ms) dispatch_time(ms) run_start run_end | |||
| ---------------------- ---------------- ---------------- ------------------- ----------- ---------- | |||
| 1 InitData 1567 100 2298200409 2298200538 | |||
| 2 GetNext 989 87 2302769932 2302769980 | |||
| 3 TruncatedNormal 1566 105 4098200409 4098200538 | |||
| AI CPU Total Time(us): 4122 | |||
| 1 InitData 1.567 0.1 2298200409 2298200538 | |||
| 2 GetNext 0.989 0.087 2302769932 2302769980 | |||
| 3 TruncatedNormal 1.566 0.105 4098200409 4098200538 | |||
| AI CPU Total Time(ms): 4.122000 | |||