From 884f12b5c8744534c054f60626e8b94916ac8f32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E9=A3=9E=E8=A6=81?= <12629886+mei-feiyao@user.noreply.gitee.com> Date: Thu, 31 Aug 2023 07:48:03 +0000 Subject: [PATCH 01/24] =?UTF-8?q?=E6=96=B0=E5=BB=BA=20affinity=5Fcpu=5Fbin?= =?UTF-8?q?d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- profiler/affinity_cpu_bind/.keep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 profiler/affinity_cpu_bind/.keep diff --git a/profiler/affinity_cpu_bind/.keep b/profiler/affinity_cpu_bind/.keep new file mode 100644 index 0000000000..e69de29bb2 -- Gitee From 348d312e9b80ef6e572e65dfe00cc62050c328d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E9=A3=9E=E8=A6=81?= <12629886+mei-feiyao@user.noreply.gitee.com> Date: Thu, 31 Aug 2023 07:51:05 +0000 Subject: [PATCH 02/24] =?UTF-8?q?update=20profiler/affinity=5Fcpu=5Fbind/b?= =?UTF-8?q?ind=5Fcore.py=20=E4=B8=8A=E4=BC=A0=E6=98=87=E8=85=BE=E4=BA=B2?= =?UTF-8?q?=E5=92=8C=E6=80=A7CPU=E7=BB=91=E6=A0=B8=E8=84=9A=E6=9C=AC?= =?UTF-8?q?=EF=BC=8C=E6=8C=89=E7=85=A7=E4=BA=B2=E5=92=8C=E6=80=A7=E7=AD=96?= =?UTF-8?q?=E7=95=A5=E4=B8=BA=E7=94=A8=E6=88=B7=E8=AE=AD=E7=BB=83=E6=88=96?= =?UTF-8?q?=E6=8E=A8=E7=90=86=E8=BF=9B=E7=A8=8B=E8=87=AA=E5=8A=A8=E7=BB=91?= =?UTF-8?q?=E6=A0=B8=EF=BC=8C=E6=A8=A1=E5=9E=8B=E5=B7=A5=E7=A8=8B=E4=B8=8D?= =?UTF-8?q?=E9=9C=80=E8=A6=81=E4=BE=B5=E5=85=A5=E5=BC=8F=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 梅飞要 <> --- profiler/affinity_cpu_bind/bind_core.py | 188 ++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 profiler/affinity_cpu_bind/bind_core.py diff --git a/profiler/affinity_cpu_bind/bind_core.py b/profiler/affinity_cpu_bind/bind_core.py new file mode 100644 index 0000000000..5453bd8386 --- /dev/null +++ b/profiler/affinity_cpu_bind/bind_core.py @@ -0,0 +1,188 @@ +#! /usr/bin/python3 +# Copyright 2023 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import re +import pwd +import sys +import time +import signal +import argparse +import datetime +import subprocess + +NPU_IDs = [] +RUNNINT_PIDs = {} +NPU_AFFINITY_CPUs = {} +CURRENT_USER = '' +BIND_CORE_RESULT_FILE = '' + +signal.signal(signal.SIGCHLD, signal.SIG_IGN) + +def create_log_file(): + time = datetime.datetime.now() + log_file = 'bind_core_' + \ + str(time.year) + '_' + \ + str(time.month) + '_' + \ + str(time.day) + '_' + \ + str(time.hour) + '_' + \ + str(time.minute) + '_' + \ + str(time.second) + '_' + '.txt' + return log_file + +def write_log_to_file(str): + pass + +def launch_cmd(cmd): + write_log_to_file('[INFO] Start to launch cmd: {}'.format(cmd)) + subprocess.Popen(cmd, shell=True) + write_log_to_file('[INFO] Succeed to launch cmd: {}.'.format(cmd)) + +def args_parse(): + parser = argparse.ArgumentParser(description='Description: Ascend Affinity-CPU-Cores Binding Script.') + parser.add_argument('-app', '--application', metavar='', nargs='+', help='Training or inference project command that you want to run. If there are more than one argument, write them in \" \".') + args = parser.parse_args() + if args.application: + application_cmd = ' '.join(args.application) + launch_cmd(application_cmd) + + +def get_current_user(): + global CURRENT_USER + CURRENT_USER = os.getuid() + +def subprocess_cmd(cmd): + res = '' + try: + res = subprocess.getoutput(cmd) + if re.search("Traceback", res): + write_log_to_file('[ERROR] Failed to run cmd: {}.'.format(cmd)) + except(Exception, TimeoutError) as err: + write_log_to_file('[ERROR] Failed to run cmd: {}.'.format(cmd)) + finally: + pass + return res + +def get_total_npu_ids(): + global NPU_IDs + + NPU_IDs = re.findall(r'\d+', subprocess_cmd('npu-smi info -l | grep "NPU ID"')) + if not NPU_IDs: + write_log_to_file('[ERROR] Failed to get npu ids on this device, please check!') + exit() + NPU_IDs = list(map(int, NPU_IDs)) + write_log_to_file('[INFO] Total npu ids on this device {}'.format(NPU_IDs)) + +def get_npu_affinity_cpus(): + global NPU_AFFINITY_CPUs + global NPU_IDs + + cpu_nums = re.findall(r'\d+', subprocess_cmd('lscpu | grep "CPU(s)"')) + if not cpu_nums: + write_log_to_file('[ERROR] Failed to get cpu nums on this device, please check!') + exit() + total_cpu_num = int(cpu_nums[0]) + node_nums = re.findall(r'\d+', subprocess_cmd('lscpu | grep "node(s)"')) + if not node_nums: + write_log_to_file('[ERROR] Failed to get node nums on this device, please check!') + exit() + total_node_num = int(node_nums[0]) + cpu_compensating = int(total_cpu_num / total_node_num) + + npu_topo_info = re.findall(r'\d+-\d+', subprocess_cmd('npu-smi info -t topo')) + if not npu_topo_info: + write_log_to_file('[ERROR] Failed to get npu topo info on this device, please check!') + exit() + + npu_index = 0 + for info in npu_topo_info: + cpu_list = info.split('-') + NPU_AFFINITY_CPUs[NPU_IDs[npu_index]] = cpu_list[0] + '-' + str(int(cpu_list[1]) + cpu_compensating) + npu_index += 1 + for k in NPU_AFFINITY_CPUs.keys: + write_log_to_file('[INFO] Affinity CPU list {} for NPU {}'.format(NPU_AFFINITY_CPUs[k], k)) + +# get npu list and affinity CPU info +def get_current_dev_info(): + get_total_npu_ids() + get_npu_affinity_cpus() + +def get_process_user(pid): + try: + stat = os.stat(f'/proc/{pid}') + return stat.st_uid + except FileNotFoundError: + return None + +#get running pid on npu +def get_running_pids_on_npu(): + global RUNNINT_PIDs + global NPU_IDs + write_log_to_file('[INFO] Start to find running pids on all NPUs') + RUNNINT_PIDs.clear() + + for times in range(5): + for id in NPU_IDs: + pids = re.findall(r'id:\d+', subprocess_cmd('npu-smi info -t proc-mem -i {} -c 0'.format(id))) + if not pids: + continue + for pid in pids: + pid = int(pid.split(':')[1]) + if get_process_user(pid) != CURRENT_USER: + continue + + if id not in RUNNINT_PIDs: + RUNNINT_PIDs[id] = pid + else: + RUNNINT_PIDs[id].append(pid) + if RUNNINT_PIDs: + return True + write_log_to_file('[WARNING] Find no running process on all NPUs, retry time {}, wait for 5 s'.format(times + 1)) + time.sleep(5) + print('[WARNING] Find no running process on all NPUs, exit') + return False + +def run_bind_cpu_cores(): + global NPU_IDs + global NPU_AFFINITY_CPUs + for npu_id, pid_list in RUNNINT_PIDs.items(): + start_cpu_id, end_cpu_id = NPU_AFFINITY_CPUs[npu_id].split('-') + + for pid in pid_list: + pid_tree = re.findall(r'(\d+)', subprocess_cmd('pstree {} -p -T'.format(pid))) + for pid in pid_tree: + current_affinity_cpu_info = subprocess_cmd('taskset -pc {}'.format(pid)) + if '-' in current_affinity_cpu_info: + current_start_cpu, current_end_cpu = re.findall(r'\d+-\d+', current_affinity_cpu_info).split('-') + if start_cpu_id == current_start_cpu and end_cpu_id == current_end_cpu: + continue + write_log_to_file('[INFO] Start to bind CPU cores for process {} on NPU {}'.format(pid, npu_id)) + res = subprocess_cmd('taskset -pc {}-{} {}'.format(start_cpu_id, end_cpu_id, pid)) + if 'failed' in res: + write_log_to_file('[WARNING] Failed to bind CPU cores for process {} on NPU {}'.format(pid, npu_id)) + write_log_to_file('[INFO] Succeed to bind CPU cores for process {} on NPU {} with CPU {}'.format(pid, npu_id, NPU_AFFINITY_CPUs[npu_id])) + +if __name__ == '__main__': + print('[INFO] Start to run Ascend_affinity_cpu_core_binding script...') + args_parse() + get_current_user() + get_current_dev_info() + + while(True): + ret = get_running_pids_on_npu() + if not ret: + exit() + run_bind_cpu_cores() + -- Gitee From 685d1554f35c81f2a1949e403f4212a81a64d7cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E9=A3=9E=E8=A6=81?= <12629886+mei-feiyao@user.noreply.gitee.com> Date: Thu, 31 Aug 2023 09:31:20 +0000 Subject: [PATCH 03/24] =?UTF-8?q?=E5=88=A0=E9=99=A4=E6=96=87=E4=BB=B6=20pr?= =?UTF-8?q?ofiler/affinity=5Fcpu=5Fbind/bind=5Fcore.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- profiler/affinity_cpu_bind/bind_core.py | 188 ------------------------ 1 file changed, 188 deletions(-) delete mode 100644 profiler/affinity_cpu_bind/bind_core.py diff --git a/profiler/affinity_cpu_bind/bind_core.py b/profiler/affinity_cpu_bind/bind_core.py deleted file mode 100644 index 5453bd8386..0000000000 --- a/profiler/affinity_cpu_bind/bind_core.py +++ /dev/null @@ -1,188 +0,0 @@ -#! /usr/bin/python3 -# Copyright 2023 Huawei Technologies Co., Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import re -import pwd -import sys -import time -import signal -import argparse -import datetime -import subprocess - -NPU_IDs = [] -RUNNINT_PIDs = {} -NPU_AFFINITY_CPUs = {} -CURRENT_USER = '' -BIND_CORE_RESULT_FILE = '' - -signal.signal(signal.SIGCHLD, signal.SIG_IGN) - -def create_log_file(): - time = datetime.datetime.now() - log_file = 'bind_core_' + \ - str(time.year) + '_' + \ - str(time.month) + '_' + \ - str(time.day) + '_' + \ - str(time.hour) + '_' + \ - str(time.minute) + '_' + \ - str(time.second) + '_' + '.txt' - return log_file - -def write_log_to_file(str): - pass - -def launch_cmd(cmd): - write_log_to_file('[INFO] Start to launch cmd: {}'.format(cmd)) - subprocess.Popen(cmd, shell=True) - write_log_to_file('[INFO] Succeed to launch cmd: {}.'.format(cmd)) - -def args_parse(): - parser = argparse.ArgumentParser(description='Description: Ascend Affinity-CPU-Cores Binding Script.') - parser.add_argument('-app', '--application', metavar='', nargs='+', help='Training or inference project command that you want to run. If there are more than one argument, write them in \" \".') - args = parser.parse_args() - if args.application: - application_cmd = ' '.join(args.application) - launch_cmd(application_cmd) - - -def get_current_user(): - global CURRENT_USER - CURRENT_USER = os.getuid() - -def subprocess_cmd(cmd): - res = '' - try: - res = subprocess.getoutput(cmd) - if re.search("Traceback", res): - write_log_to_file('[ERROR] Failed to run cmd: {}.'.format(cmd)) - except(Exception, TimeoutError) as err: - write_log_to_file('[ERROR] Failed to run cmd: {}.'.format(cmd)) - finally: - pass - return res - -def get_total_npu_ids(): - global NPU_IDs - - NPU_IDs = re.findall(r'\d+', subprocess_cmd('npu-smi info -l | grep "NPU ID"')) - if not NPU_IDs: - write_log_to_file('[ERROR] Failed to get npu ids on this device, please check!') - exit() - NPU_IDs = list(map(int, NPU_IDs)) - write_log_to_file('[INFO] Total npu ids on this device {}'.format(NPU_IDs)) - -def get_npu_affinity_cpus(): - global NPU_AFFINITY_CPUs - global NPU_IDs - - cpu_nums = re.findall(r'\d+', subprocess_cmd('lscpu | grep "CPU(s)"')) - if not cpu_nums: - write_log_to_file('[ERROR] Failed to get cpu nums on this device, please check!') - exit() - total_cpu_num = int(cpu_nums[0]) - node_nums = re.findall(r'\d+', subprocess_cmd('lscpu | grep "node(s)"')) - if not node_nums: - write_log_to_file('[ERROR] Failed to get node nums on this device, please check!') - exit() - total_node_num = int(node_nums[0]) - cpu_compensating = int(total_cpu_num / total_node_num) - - npu_topo_info = re.findall(r'\d+-\d+', subprocess_cmd('npu-smi info -t topo')) - if not npu_topo_info: - write_log_to_file('[ERROR] Failed to get npu topo info on this device, please check!') - exit() - - npu_index = 0 - for info in npu_topo_info: - cpu_list = info.split('-') - NPU_AFFINITY_CPUs[NPU_IDs[npu_index]] = cpu_list[0] + '-' + str(int(cpu_list[1]) + cpu_compensating) - npu_index += 1 - for k in NPU_AFFINITY_CPUs.keys: - write_log_to_file('[INFO] Affinity CPU list {} for NPU {}'.format(NPU_AFFINITY_CPUs[k], k)) - -# get npu list and affinity CPU info -def get_current_dev_info(): - get_total_npu_ids() - get_npu_affinity_cpus() - -def get_process_user(pid): - try: - stat = os.stat(f'/proc/{pid}') - return stat.st_uid - except FileNotFoundError: - return None - -#get running pid on npu -def get_running_pids_on_npu(): - global RUNNINT_PIDs - global NPU_IDs - write_log_to_file('[INFO] Start to find running pids on all NPUs') - RUNNINT_PIDs.clear() - - for times in range(5): - for id in NPU_IDs: - pids = re.findall(r'id:\d+', subprocess_cmd('npu-smi info -t proc-mem -i {} -c 0'.format(id))) - if not pids: - continue - for pid in pids: - pid = int(pid.split(':')[1]) - if get_process_user(pid) != CURRENT_USER: - continue - - if id not in RUNNINT_PIDs: - RUNNINT_PIDs[id] = pid - else: - RUNNINT_PIDs[id].append(pid) - if RUNNINT_PIDs: - return True - write_log_to_file('[WARNING] Find no running process on all NPUs, retry time {}, wait for 5 s'.format(times + 1)) - time.sleep(5) - print('[WARNING] Find no running process on all NPUs, exit') - return False - -def run_bind_cpu_cores(): - global NPU_IDs - global NPU_AFFINITY_CPUs - for npu_id, pid_list in RUNNINT_PIDs.items(): - start_cpu_id, end_cpu_id = NPU_AFFINITY_CPUs[npu_id].split('-') - - for pid in pid_list: - pid_tree = re.findall(r'(\d+)', subprocess_cmd('pstree {} -p -T'.format(pid))) - for pid in pid_tree: - current_affinity_cpu_info = subprocess_cmd('taskset -pc {}'.format(pid)) - if '-' in current_affinity_cpu_info: - current_start_cpu, current_end_cpu = re.findall(r'\d+-\d+', current_affinity_cpu_info).split('-') - if start_cpu_id == current_start_cpu and end_cpu_id == current_end_cpu: - continue - write_log_to_file('[INFO] Start to bind CPU cores for process {} on NPU {}'.format(pid, npu_id)) - res = subprocess_cmd('taskset -pc {}-{} {}'.format(start_cpu_id, end_cpu_id, pid)) - if 'failed' in res: - write_log_to_file('[WARNING] Failed to bind CPU cores for process {} on NPU {}'.format(pid, npu_id)) - write_log_to_file('[INFO] Succeed to bind CPU cores for process {} on NPU {} with CPU {}'.format(pid, npu_id, NPU_AFFINITY_CPUs[npu_id])) - -if __name__ == '__main__': - print('[INFO] Start to run Ascend_affinity_cpu_core_binding script...') - args_parse() - get_current_user() - get_current_dev_info() - - while(True): - ret = get_running_pids_on_npu() - if not ret: - exit() - run_bind_cpu_cores() - -- Gitee From b5b2e0ef6db75316ca356328e72e013760d5ee3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E9=A3=9E=E8=A6=81?= <12629886+mei-feiyao@user.noreply.gitee.com> Date: Thu, 31 Aug 2023 14:17:15 +0000 Subject: [PATCH 04/24] update profiler/affinity_cpu_bind/bind_core.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 梅飞要 <> --- profiler/affinity_cpu_bind/bind_core.py | 192 ++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 profiler/affinity_cpu_bind/bind_core.py diff --git a/profiler/affinity_cpu_bind/bind_core.py b/profiler/affinity_cpu_bind/bind_core.py new file mode 100644 index 0000000000..8036300c95 --- /dev/null +++ b/profiler/affinity_cpu_bind/bind_core.py @@ -0,0 +1,192 @@ +#! /usr/bin/python3 +# Copyright 2023 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import re +import pwd +import sys +import time +import signal +import argparse +import datetime +import subprocess + +NPU_IDs = [] +RUNNINT_PIDs = {} +NPU_AFFINITY_CPUs = {} +CURRENT_USER = '' +BIND_CORE_RESULT_FILE = '' + +signal.signal(signal.SIGCHLD, signal.SIG_IGN) + +def create_log_file(): + global BIND_CORE_RESULT_FILE + time = datetime.datetime.now() + BIND_CORE_RESULT_FILE = 'bind_core_' + \ + str(time.year) + '_' + \ + str(time.month) + '_' + \ + str(time.day) + '_' + \ + str(time.hour) + '_' + \ + str(time.minute) + '_' + \ + str(time.second) + '_' + '.txt' + +def write_log_to_file(str): + global BIND_CORE_RESULT_FILE + file = open(BIND_CORE_RESULT_FILE, 'w') + file.write(str) + file.close() + +def launch_cmd(cmd): + write_log_to_file('[INFO] Start to launch cmd: {}'.format(cmd)) + subprocess.Popen(cmd, shell=True) + write_log_to_file('[INFO] Succeed to launch cmd: {}.'.format(cmd)) + +def args_parse(): + parser = argparse.ArgumentParser(description='Description: Ascend Affinity-CPU-Cores Binding Script.') + parser.add_argument('-app', '--application', metavar='', nargs='+', help='Training or inference project command that you want to run. If there are more than one argument, write them in \" \".') + args = parser.parse_args() + if args.application: + application_cmd = ' '.join(args.application) + launch_cmd(application_cmd) + + +def get_current_user(): + global CURRENT_USER + CURRENT_USER = os.getuid() + +def subprocess_cmd(cmd): + res = '' + try: + res = subprocess.getoutput(cmd) + if re.search("Traceback", res): + write_log_to_file('[ERROR] Failed to run cmd: {}.'.format(cmd)) + except(Exception, TimeoutError) as err: + write_log_to_file('[ERROR] Failed to run cmd: {}.'.format(cmd)) + finally: + pass + return res + +def get_total_npu_ids(): + global NPU_IDs + + NPU_IDs = re.findall(r'\d+', subprocess_cmd('npu-smi info -l | grep "NPU ID"')) + if not NPU_IDs: + write_log_to_file('[ERROR] Failed to get npu ids on this device, please check!') + exit() + NPU_IDs = list(map(int, NPU_IDs)) + write_log_to_file('[INFO] Total npu ids on this device {}'.format(NPU_IDs)) + +def get_npu_affinity_cpus(): + global NPU_AFFINITY_CPUs + global NPU_IDs + + cpu_nums = re.findall(r'\d+', subprocess_cmd('lscpu | grep "CPU(s)"')) + if not cpu_nums: + write_log_to_file('[ERROR] Failed to get cpu nums on this device, please check!') + exit() + total_cpu_num = int(cpu_nums[0]) + node_nums = re.findall(r'\d+', subprocess_cmd('lscpu | grep "node(s)"')) + if not node_nums: + write_log_to_file('[ERROR] Failed to get node nums on this device, please check!') + exit() + total_node_num = int(node_nums[0]) + cpu_compensating = int(total_cpu_num / total_node_num) + + npu_topo_info = re.findall(r'\d+-\d+', subprocess_cmd('npu-smi info -t topo')) + if not npu_topo_info: + write_log_to_file('[ERROR] Failed to get npu topo info on this device, please check!') + exit() + + npu_index = 0 + for info in npu_topo_info: + cpu_list = info.split('-') + NPU_AFFINITY_CPUs[NPU_IDs[npu_index]] = cpu_list[0] + '-' + str(int(cpu_list[1]) + cpu_compensating) + npu_index += 1 + for k in NPU_AFFINITY_CPUs.keys: + write_log_to_file('[INFO] Affinity CPU list {} for NPU {}'.format(NPU_AFFINITY_CPUs[k], k)) + +# get npu list and affinity CPU info +def get_current_dev_info(): + get_total_npu_ids() + get_npu_affinity_cpus() + +def get_process_user(pid): + try: + stat = os.stat(f'/proc/{pid}') + return stat.st_uid + except FileNotFoundError: + return None + +#get running pid on npu +def get_running_pids_on_npu(): + global RUNNINT_PIDs + global NPU_IDs + write_log_to_file('[INFO] Start to find running pids on all NPUs') + RUNNINT_PIDs.clear() + + for times in range(5): + for id in NPU_IDs: + pids = re.findall(r'id:\d+', subprocess_cmd('npu-smi info -t proc-mem -i {} -c 0'.format(id))) + if not pids: + continue + for pid in pids: + pid = int(pid.split(':')[1]) + if get_process_user(pid) != CURRENT_USER: + continue + + if id not in RUNNINT_PIDs: + RUNNINT_PIDs[id] = pid + else: + RUNNINT_PIDs[id].append(pid) + if RUNNINT_PIDs: + return True + write_log_to_file('[WARNING] Find no running process on all NPUs, retry time {}, wait for 5 s'.format(times + 1)) + time.sleep(5) + print('[WARNING] Find no running process on all NPUs, exit') + return False + +def run_bind_cpu_cores(): + global NPU_IDs + global NPU_AFFINITY_CPUs + for npu_id, pid_list in RUNNINT_PIDs.items(): + start_cpu_id, end_cpu_id = NPU_AFFINITY_CPUs[npu_id].split('-') + + for pid in pid_list: + pid_tree = re.findall(r'(\d+)', subprocess_cmd('pstree {} -p -T'.format(pid))) + for pid in pid_tree: + current_affinity_cpu_info = subprocess_cmd('taskset -pc {}'.format(pid)) + if '-' in current_affinity_cpu_info: + current_start_cpu, current_end_cpu = re.findall(r'\d+-\d+', current_affinity_cpu_info).split('-') + if start_cpu_id == current_start_cpu and end_cpu_id == current_end_cpu: + continue + write_log_to_file('[INFO] Start to bind CPU cores for process {} on NPU {}'.format(pid, npu_id)) + res = subprocess_cmd('taskset -pc {}-{} {}'.format(start_cpu_id, end_cpu_id, pid)) + if 'failed' in res: + write_log_to_file('[WARNING] Failed to bind CPU cores for process {} on NPU {}'.format(pid, npu_id)) + write_log_to_file('[INFO] Succeed to bind CPU cores for process {} on NPU {} with CPU {}'.format(pid, npu_id, NPU_AFFINITY_CPUs[npu_id])) + +if __name__ == '__main__': + print('[INFO] Start to run Ascend_affinity_cpu_core_binding script...') + create_log_file() + args_parse() + get_current_user() + get_current_dev_info() + + while(True): + ret = get_running_pids_on_npu() + if not ret: + exit() + run_bind_cpu_cores() + -- Gitee From b11b9c1bf06a70267860f371e32745ca717054cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E9=A3=9E=E8=A6=81?= <12629886+mei-feiyao@user.noreply.gitee.com> Date: Fri, 1 Sep 2023 05:09:35 +0000 Subject: [PATCH 05/24] =?UTF-8?q?=E5=88=A0=E9=99=A4=E6=96=87=E4=BB=B6=20pr?= =?UTF-8?q?ofiler/affinity=5Fcpu=5Fbind/bind=5Fcore.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- profiler/affinity_cpu_bind/bind_core.py | 192 ------------------------ 1 file changed, 192 deletions(-) delete mode 100644 profiler/affinity_cpu_bind/bind_core.py diff --git a/profiler/affinity_cpu_bind/bind_core.py b/profiler/affinity_cpu_bind/bind_core.py deleted file mode 100644 index 8036300c95..0000000000 --- a/profiler/affinity_cpu_bind/bind_core.py +++ /dev/null @@ -1,192 +0,0 @@ -#! /usr/bin/python3 -# Copyright 2023 Huawei Technologies Co., Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import re -import pwd -import sys -import time -import signal -import argparse -import datetime -import subprocess - -NPU_IDs = [] -RUNNINT_PIDs = {} -NPU_AFFINITY_CPUs = {} -CURRENT_USER = '' -BIND_CORE_RESULT_FILE = '' - -signal.signal(signal.SIGCHLD, signal.SIG_IGN) - -def create_log_file(): - global BIND_CORE_RESULT_FILE - time = datetime.datetime.now() - BIND_CORE_RESULT_FILE = 'bind_core_' + \ - str(time.year) + '_' + \ - str(time.month) + '_' + \ - str(time.day) + '_' + \ - str(time.hour) + '_' + \ - str(time.minute) + '_' + \ - str(time.second) + '_' + '.txt' - -def write_log_to_file(str): - global BIND_CORE_RESULT_FILE - file = open(BIND_CORE_RESULT_FILE, 'w') - file.write(str) - file.close() - -def launch_cmd(cmd): - write_log_to_file('[INFO] Start to launch cmd: {}'.format(cmd)) - subprocess.Popen(cmd, shell=True) - write_log_to_file('[INFO] Succeed to launch cmd: {}.'.format(cmd)) - -def args_parse(): - parser = argparse.ArgumentParser(description='Description: Ascend Affinity-CPU-Cores Binding Script.') - parser.add_argument('-app', '--application', metavar='', nargs='+', help='Training or inference project command that you want to run. If there are more than one argument, write them in \" \".') - args = parser.parse_args() - if args.application: - application_cmd = ' '.join(args.application) - launch_cmd(application_cmd) - - -def get_current_user(): - global CURRENT_USER - CURRENT_USER = os.getuid() - -def subprocess_cmd(cmd): - res = '' - try: - res = subprocess.getoutput(cmd) - if re.search("Traceback", res): - write_log_to_file('[ERROR] Failed to run cmd: {}.'.format(cmd)) - except(Exception, TimeoutError) as err: - write_log_to_file('[ERROR] Failed to run cmd: {}.'.format(cmd)) - finally: - pass - return res - -def get_total_npu_ids(): - global NPU_IDs - - NPU_IDs = re.findall(r'\d+', subprocess_cmd('npu-smi info -l | grep "NPU ID"')) - if not NPU_IDs: - write_log_to_file('[ERROR] Failed to get npu ids on this device, please check!') - exit() - NPU_IDs = list(map(int, NPU_IDs)) - write_log_to_file('[INFO] Total npu ids on this device {}'.format(NPU_IDs)) - -def get_npu_affinity_cpus(): - global NPU_AFFINITY_CPUs - global NPU_IDs - - cpu_nums = re.findall(r'\d+', subprocess_cmd('lscpu | grep "CPU(s)"')) - if not cpu_nums: - write_log_to_file('[ERROR] Failed to get cpu nums on this device, please check!') - exit() - total_cpu_num = int(cpu_nums[0]) - node_nums = re.findall(r'\d+', subprocess_cmd('lscpu | grep "node(s)"')) - if not node_nums: - write_log_to_file('[ERROR] Failed to get node nums on this device, please check!') - exit() - total_node_num = int(node_nums[0]) - cpu_compensating = int(total_cpu_num / total_node_num) - - npu_topo_info = re.findall(r'\d+-\d+', subprocess_cmd('npu-smi info -t topo')) - if not npu_topo_info: - write_log_to_file('[ERROR] Failed to get npu topo info on this device, please check!') - exit() - - npu_index = 0 - for info in npu_topo_info: - cpu_list = info.split('-') - NPU_AFFINITY_CPUs[NPU_IDs[npu_index]] = cpu_list[0] + '-' + str(int(cpu_list[1]) + cpu_compensating) - npu_index += 1 - for k in NPU_AFFINITY_CPUs.keys: - write_log_to_file('[INFO] Affinity CPU list {} for NPU {}'.format(NPU_AFFINITY_CPUs[k], k)) - -# get npu list and affinity CPU info -def get_current_dev_info(): - get_total_npu_ids() - get_npu_affinity_cpus() - -def get_process_user(pid): - try: - stat = os.stat(f'/proc/{pid}') - return stat.st_uid - except FileNotFoundError: - return None - -#get running pid on npu -def get_running_pids_on_npu(): - global RUNNINT_PIDs - global NPU_IDs - write_log_to_file('[INFO] Start to find running pids on all NPUs') - RUNNINT_PIDs.clear() - - for times in range(5): - for id in NPU_IDs: - pids = re.findall(r'id:\d+', subprocess_cmd('npu-smi info -t proc-mem -i {} -c 0'.format(id))) - if not pids: - continue - for pid in pids: - pid = int(pid.split(':')[1]) - if get_process_user(pid) != CURRENT_USER: - continue - - if id not in RUNNINT_PIDs: - RUNNINT_PIDs[id] = pid - else: - RUNNINT_PIDs[id].append(pid) - if RUNNINT_PIDs: - return True - write_log_to_file('[WARNING] Find no running process on all NPUs, retry time {}, wait for 5 s'.format(times + 1)) - time.sleep(5) - print('[WARNING] Find no running process on all NPUs, exit') - return False - -def run_bind_cpu_cores(): - global NPU_IDs - global NPU_AFFINITY_CPUs - for npu_id, pid_list in RUNNINT_PIDs.items(): - start_cpu_id, end_cpu_id = NPU_AFFINITY_CPUs[npu_id].split('-') - - for pid in pid_list: - pid_tree = re.findall(r'(\d+)', subprocess_cmd('pstree {} -p -T'.format(pid))) - for pid in pid_tree: - current_affinity_cpu_info = subprocess_cmd('taskset -pc {}'.format(pid)) - if '-' in current_affinity_cpu_info: - current_start_cpu, current_end_cpu = re.findall(r'\d+-\d+', current_affinity_cpu_info).split('-') - if start_cpu_id == current_start_cpu and end_cpu_id == current_end_cpu: - continue - write_log_to_file('[INFO] Start to bind CPU cores for process {} on NPU {}'.format(pid, npu_id)) - res = subprocess_cmd('taskset -pc {}-{} {}'.format(start_cpu_id, end_cpu_id, pid)) - if 'failed' in res: - write_log_to_file('[WARNING] Failed to bind CPU cores for process {} on NPU {}'.format(pid, npu_id)) - write_log_to_file('[INFO] Succeed to bind CPU cores for process {} on NPU {} with CPU {}'.format(pid, npu_id, NPU_AFFINITY_CPUs[npu_id])) - -if __name__ == '__main__': - print('[INFO] Start to run Ascend_affinity_cpu_core_binding script...') - create_log_file() - args_parse() - get_current_user() - get_current_dev_info() - - while(True): - ret = get_running_pids_on_npu() - if not ret: - exit() - run_bind_cpu_cores() - -- Gitee From a88e1ca26110a42e67d6ce842bc0fbaf9f37ceb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E9=A3=9E=E8=A6=81?= <12629886+mei-feiyao@user.noreply.gitee.com> Date: Fri, 1 Sep 2023 05:09:55 +0000 Subject: [PATCH 06/24] update profiler/affinity_cpu_bind/bind_core.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 梅飞要 <> --- profiler/affinity_cpu_bind/bind_core.py | 192 ++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 profiler/affinity_cpu_bind/bind_core.py diff --git a/profiler/affinity_cpu_bind/bind_core.py b/profiler/affinity_cpu_bind/bind_core.py new file mode 100644 index 0000000000..8036300c95 --- /dev/null +++ b/profiler/affinity_cpu_bind/bind_core.py @@ -0,0 +1,192 @@ +#! /usr/bin/python3 +# Copyright 2023 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import re +import pwd +import sys +import time +import signal +import argparse +import datetime +import subprocess + +NPU_IDs = [] +RUNNINT_PIDs = {} +NPU_AFFINITY_CPUs = {} +CURRENT_USER = '' +BIND_CORE_RESULT_FILE = '' + +signal.signal(signal.SIGCHLD, signal.SIG_IGN) + +def create_log_file(): + global BIND_CORE_RESULT_FILE + time = datetime.datetime.now() + BIND_CORE_RESULT_FILE = 'bind_core_' + \ + str(time.year) + '_' + \ + str(time.month) + '_' + \ + str(time.day) + '_' + \ + str(time.hour) + '_' + \ + str(time.minute) + '_' + \ + str(time.second) + '_' + '.txt' + +def write_log_to_file(str): + global BIND_CORE_RESULT_FILE + file = open(BIND_CORE_RESULT_FILE, 'w') + file.write(str) + file.close() + +def launch_cmd(cmd): + write_log_to_file('[INFO] Start to launch cmd: {}'.format(cmd)) + subprocess.Popen(cmd, shell=True) + write_log_to_file('[INFO] Succeed to launch cmd: {}.'.format(cmd)) + +def args_parse(): + parser = argparse.ArgumentParser(description='Description: Ascend Affinity-CPU-Cores Binding Script.') + parser.add_argument('-app', '--application', metavar='', nargs='+', help='Training or inference project command that you want to run. If there are more than one argument, write them in \" \".') + args = parser.parse_args() + if args.application: + application_cmd = ' '.join(args.application) + launch_cmd(application_cmd) + + +def get_current_user(): + global CURRENT_USER + CURRENT_USER = os.getuid() + +def subprocess_cmd(cmd): + res = '' + try: + res = subprocess.getoutput(cmd) + if re.search("Traceback", res): + write_log_to_file('[ERROR] Failed to run cmd: {}.'.format(cmd)) + except(Exception, TimeoutError) as err: + write_log_to_file('[ERROR] Failed to run cmd: {}.'.format(cmd)) + finally: + pass + return res + +def get_total_npu_ids(): + global NPU_IDs + + NPU_IDs = re.findall(r'\d+', subprocess_cmd('npu-smi info -l | grep "NPU ID"')) + if not NPU_IDs: + write_log_to_file('[ERROR] Failed to get npu ids on this device, please check!') + exit() + NPU_IDs = list(map(int, NPU_IDs)) + write_log_to_file('[INFO] Total npu ids on this device {}'.format(NPU_IDs)) + +def get_npu_affinity_cpus(): + global NPU_AFFINITY_CPUs + global NPU_IDs + + cpu_nums = re.findall(r'\d+', subprocess_cmd('lscpu | grep "CPU(s)"')) + if not cpu_nums: + write_log_to_file('[ERROR] Failed to get cpu nums on this device, please check!') + exit() + total_cpu_num = int(cpu_nums[0]) + node_nums = re.findall(r'\d+', subprocess_cmd('lscpu | grep "node(s)"')) + if not node_nums: + write_log_to_file('[ERROR] Failed to get node nums on this device, please check!') + exit() + total_node_num = int(node_nums[0]) + cpu_compensating = int(total_cpu_num / total_node_num) + + npu_topo_info = re.findall(r'\d+-\d+', subprocess_cmd('npu-smi info -t topo')) + if not npu_topo_info: + write_log_to_file('[ERROR] Failed to get npu topo info on this device, please check!') + exit() + + npu_index = 0 + for info in npu_topo_info: + cpu_list = info.split('-') + NPU_AFFINITY_CPUs[NPU_IDs[npu_index]] = cpu_list[0] + '-' + str(int(cpu_list[1]) + cpu_compensating) + npu_index += 1 + for k in NPU_AFFINITY_CPUs.keys: + write_log_to_file('[INFO] Affinity CPU list {} for NPU {}'.format(NPU_AFFINITY_CPUs[k], k)) + +# get npu list and affinity CPU info +def get_current_dev_info(): + get_total_npu_ids() + get_npu_affinity_cpus() + +def get_process_user(pid): + try: + stat = os.stat(f'/proc/{pid}') + return stat.st_uid + except FileNotFoundError: + return None + +#get running pid on npu +def get_running_pids_on_npu(): + global RUNNINT_PIDs + global NPU_IDs + write_log_to_file('[INFO] Start to find running pids on all NPUs') + RUNNINT_PIDs.clear() + + for times in range(5): + for id in NPU_IDs: + pids = re.findall(r'id:\d+', subprocess_cmd('npu-smi info -t proc-mem -i {} -c 0'.format(id))) + if not pids: + continue + for pid in pids: + pid = int(pid.split(':')[1]) + if get_process_user(pid) != CURRENT_USER: + continue + + if id not in RUNNINT_PIDs: + RUNNINT_PIDs[id] = pid + else: + RUNNINT_PIDs[id].append(pid) + if RUNNINT_PIDs: + return True + write_log_to_file('[WARNING] Find no running process on all NPUs, retry time {}, wait for 5 s'.format(times + 1)) + time.sleep(5) + print('[WARNING] Find no running process on all NPUs, exit') + return False + +def run_bind_cpu_cores(): + global NPU_IDs + global NPU_AFFINITY_CPUs + for npu_id, pid_list in RUNNINT_PIDs.items(): + start_cpu_id, end_cpu_id = NPU_AFFINITY_CPUs[npu_id].split('-') + + for pid in pid_list: + pid_tree = re.findall(r'(\d+)', subprocess_cmd('pstree {} -p -T'.format(pid))) + for pid in pid_tree: + current_affinity_cpu_info = subprocess_cmd('taskset -pc {}'.format(pid)) + if '-' in current_affinity_cpu_info: + current_start_cpu, current_end_cpu = re.findall(r'\d+-\d+', current_affinity_cpu_info).split('-') + if start_cpu_id == current_start_cpu and end_cpu_id == current_end_cpu: + continue + write_log_to_file('[INFO] Start to bind CPU cores for process {} on NPU {}'.format(pid, npu_id)) + res = subprocess_cmd('taskset -pc {}-{} {}'.format(start_cpu_id, end_cpu_id, pid)) + if 'failed' in res: + write_log_to_file('[WARNING] Failed to bind CPU cores for process {} on NPU {}'.format(pid, npu_id)) + write_log_to_file('[INFO] Succeed to bind CPU cores for process {} on NPU {} with CPU {}'.format(pid, npu_id, NPU_AFFINITY_CPUs[npu_id])) + +if __name__ == '__main__': + print('[INFO] Start to run Ascend_affinity_cpu_core_binding script...') + create_log_file() + args_parse() + get_current_user() + get_current_dev_info() + + while(True): + ret = get_running_pids_on_npu() + if not ret: + exit() + run_bind_cpu_cores() + -- Gitee From ce7011b95ec725eee0368492f7b838ba62ea23d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E9=A3=9E=E8=A6=81?= Date: Tue, 28 Nov 2023 09:56:23 +0000 Subject: [PATCH 07/24] update profiler/affinity_cpu_bind/bind_core.py. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 梅飞要 --- profiler/affinity_cpu_bind/bind_core.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/profiler/affinity_cpu_bind/bind_core.py b/profiler/affinity_cpu_bind/bind_core.py index 8036300c95..5c7a440384 100644 --- a/profiler/affinity_cpu_bind/bind_core.py +++ b/profiler/affinity_cpu_bind/bind_core.py @@ -114,7 +114,7 @@ def get_npu_affinity_cpus(): cpu_list = info.split('-') NPU_AFFINITY_CPUs[NPU_IDs[npu_index]] = cpu_list[0] + '-' + str(int(cpu_list[1]) + cpu_compensating) npu_index += 1 - for k in NPU_AFFINITY_CPUs.keys: + for k in NPU_AFFINITY_CPUs.keys(): write_log_to_file('[INFO] Affinity CPU list {} for NPU {}'.format(NPU_AFFINITY_CPUs[k], k)) # get npu list and affinity CPU info @@ -147,7 +147,7 @@ def get_running_pids_on_npu(): continue if id not in RUNNINT_PIDs: - RUNNINT_PIDs[id] = pid + RUNNINT_PIDs[id] = [ pid ] else: RUNNINT_PIDs[id].append(pid) if RUNNINT_PIDs: @@ -168,7 +168,7 @@ def run_bind_cpu_cores(): for pid in pid_tree: current_affinity_cpu_info = subprocess_cmd('taskset -pc {}'.format(pid)) if '-' in current_affinity_cpu_info: - current_start_cpu, current_end_cpu = re.findall(r'\d+-\d+', current_affinity_cpu_info).split('-') + current_start_cpu, current_end_cpu = current_affinity_cpu_info.split(' ')[-1].split('-') if start_cpu_id == current_start_cpu and end_cpu_id == current_end_cpu: continue write_log_to_file('[INFO] Start to bind CPU cores for process {} on NPU {}'.format(pid, npu_id)) -- Gitee From b661914c544ad46b7712ccfdff35e4256e48d1ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E9=A3=9E=E8=A6=81?= Date: Tue, 28 Nov 2023 10:07:19 +0000 Subject: [PATCH 08/24] =?UTF-8?q?=E5=88=A0=E9=99=A4=E6=96=87=E4=BB=B6=20pr?= =?UTF-8?q?ofiler/affinity=5Fcpu=5Fbind/.keep?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- profiler/affinity_cpu_bind/.keep | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 profiler/affinity_cpu_bind/.keep diff --git a/profiler/affinity_cpu_bind/.keep b/profiler/affinity_cpu_bind/.keep deleted file mode 100644 index e69de29bb2..0000000000 -- Gitee From bf773a0043d301706c93a66e2eadf6441e341e98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E9=A3=9E=E8=A6=81?= Date: Tue, 28 Nov 2023 10:07:45 +0000 Subject: [PATCH 09/24] add profiler/affinity_cpu_bind/README.md. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 梅飞要 --- profiler/affinity_cpu_bind/README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 profiler/affinity_cpu_bind/README.md diff --git a/profiler/affinity_cpu_bind/README.md b/profiler/affinity_cpu_bind/README.md new file mode 100644 index 0000000000..9ccfa5629a --- /dev/null +++ b/profiler/affinity_cpu_bind/README.md @@ -0,0 +1 @@ +增加readme.md \ No newline at end of file -- Gitee From 0d2dd5fb3fa22ad4bff0bc3116e9fde030c3acb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E9=A3=9E=E8=A6=81?= Date: Tue, 28 Nov 2023 10:09:08 +0000 Subject: [PATCH 10/24] update profiler/affinity_cpu_bind/README.md. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 梅飞要 --- profiler/affinity_cpu_bind/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/profiler/affinity_cpu_bind/README.md b/profiler/affinity_cpu_bind/README.md index 9ccfa5629a..9ed00225e8 100644 --- a/profiler/affinity_cpu_bind/README.md +++ b/profiler/affinity_cpu_bind/README.md @@ -1 +1,2 @@ -增加readme.md \ No newline at end of file + **昇腾亲和性CPU绑核工具** + **介绍** -- Gitee From a509b95404d4293891d98c98e2610552cbb1c7d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E9=A3=9E=E8=A6=81?= Date: Tue, 28 Nov 2023 10:16:52 +0000 Subject: [PATCH 11/24] update profiler/affinity_cpu_bind/README.md. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 梅飞要 --- profiler/affinity_cpu_bind/README.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/profiler/affinity_cpu_bind/README.md b/profiler/affinity_cpu_bind/README.md index 9ed00225e8..105a226468 100644 --- a/profiler/affinity_cpu_bind/README.md +++ b/profiler/affinity_cpu_bind/README.md @@ -1,2 +1,13 @@ - **昇腾亲和性CPU绑核工具** - **介绍** +### **昇腾亲和性CPU绑核工具** + +### **介绍** +昇腾亲和性CPU绑核工具支持用户无需侵入式修改工程,直接运行工具即可实现按亲和性策略绑核,提升推理或训练性能。 + +### **使用方式** + + + + + + + -- Gitee From d96c45e73afa982391a51ed06ecd1e60e5532220 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E9=A3=9E=E8=A6=81?= Date: Tue, 28 Nov 2023 10:29:16 +0000 Subject: [PATCH 12/24] update profiler/affinity_cpu_bind/README.md. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 梅飞要 --- profiler/affinity_cpu_bind/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/profiler/affinity_cpu_bind/README.md b/profiler/affinity_cpu_bind/README.md index 105a226468..d1022e7424 100644 --- a/profiler/affinity_cpu_bind/README.md +++ b/profiler/affinity_cpu_bind/README.md @@ -4,6 +4,16 @@ 昇腾亲和性CPU绑核工具支持用户无需侵入式修改工程,直接运行工具即可实现按亲和性策略绑核,提升推理或训练性能。 ### **使用方式** +1. + +2. + +### **使用须知 +1.使用工具前应提前安装pstree工具,参考命令yum install -y psmisc或apt -y install psmisc +2.使用前手动执行npu-smi info -t topo,出现如下样例结果,说明环境支持绑核,否则请将环境驱动包升级到 + + + -- Gitee From 8b5fab58c19938af1b52f05667666f8e2a80534c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E9=A3=9E=E8=A6=81?= Date: Tue, 28 Nov 2023 10:43:27 +0000 Subject: [PATCH 13/24] update profiler/affinity_cpu_bind/README.md. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 梅飞要 --- profiler/affinity_cpu_bind/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/profiler/affinity_cpu_bind/README.md b/profiler/affinity_cpu_bind/README.md index d1022e7424..9f89fdb2e9 100644 --- a/profiler/affinity_cpu_bind/README.md +++ b/profiler/affinity_cpu_bind/README.md @@ -10,6 +10,7 @@ ### **使用须知 1.使用工具前应提前安装pstree工具,参考命令yum install -y psmisc或apt -y install psmisc + 2.使用前手动执行npu-smi info -t topo,出现如下样例结果,说明环境支持绑核,否则请将环境驱动包升级到 -- Gitee From 79a324fe2f39cc1e7671366bc9c5e5d4f2cae712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E9=A3=9E=E8=A6=81?= Date: Tue, 28 Nov 2023 10:44:02 +0000 Subject: [PATCH 14/24] update profiler/affinity_cpu_bind/README.md. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 梅飞要 --- profiler/affinity_cpu_bind/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/profiler/affinity_cpu_bind/README.md b/profiler/affinity_cpu_bind/README.md index 9f89fdb2e9..881079b2ab 100644 --- a/profiler/affinity_cpu_bind/README.md +++ b/profiler/affinity_cpu_bind/README.md @@ -12,6 +12,16 @@ 1.使用工具前应提前安装pstree工具,参考命令yum install -y psmisc或apt -y install psmisc 2.使用前手动执行npu-smi info -t topo,出现如下样例结果,说明环境支持绑核,否则请将环境驱动包升级到 + NPU0 NPU1 NPU2 NPU3 NPU4 NPU5 NPU6 NPU7 CPU Affinity +NPU0 X HCCS HCCS HCCS PHB SYS SYS SYS 144-167 +NPU1 HCCS X HCCS HCCS SYS PHB SYS SYS 96-119 +NPU2 HCCS HCCS X HCCS SYS SYS PHB SYS 48-71 +NPU3 HCCS HCCS HCCS X SYS SYS SYS PHB 0-23 +NPU4 PHB SYS SYS SYS X HCCS HCCS HCCS 144-167 +NPU5 SYS PHB SYS SYS HCCS X HCCS HCCS 96-119 +NPU6 SYS SYS PHB SYS HCCS HCCS X HCCS 48-71 +NPU7 SYS SYS SYS PHB HCCS HCCS HCCS X 0-23 + -- Gitee From fd7624e1e89e83eb3ccd408c91a8041c9fb3b487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E9=A3=9E=E8=A6=81?= Date: Tue, 28 Nov 2023 10:44:33 +0000 Subject: [PATCH 15/24] update profiler/affinity_cpu_bind/README.md. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 梅飞要 --- profiler/affinity_cpu_bind/README.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/profiler/affinity_cpu_bind/README.md b/profiler/affinity_cpu_bind/README.md index 881079b2ab..a2b865e14e 100644 --- a/profiler/affinity_cpu_bind/README.md +++ b/profiler/affinity_cpu_bind/README.md @@ -12,15 +12,6 @@ 1.使用工具前应提前安装pstree工具,参考命令yum install -y psmisc或apt -y install psmisc 2.使用前手动执行npu-smi info -t topo,出现如下样例结果,说明环境支持绑核,否则请将环境驱动包升级到 - NPU0 NPU1 NPU2 NPU3 NPU4 NPU5 NPU6 NPU7 CPU Affinity -NPU0 X HCCS HCCS HCCS PHB SYS SYS SYS 144-167 -NPU1 HCCS X HCCS HCCS SYS PHB SYS SYS 96-119 -NPU2 HCCS HCCS X HCCS SYS SYS PHB SYS 48-71 -NPU3 HCCS HCCS HCCS X SYS SYS SYS PHB 0-23 -NPU4 PHB SYS SYS SYS X HCCS HCCS HCCS 144-167 -NPU5 SYS PHB SYS SYS HCCS X HCCS HCCS 96-119 -NPU6 SYS SYS PHB SYS HCCS HCCS X HCCS 48-71 -NPU7 SYS SYS SYS PHB HCCS HCCS HCCS X 0-23 -- Gitee From b34eb5c7a4daf6b5591a6be66a81bc6710e73d0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E9=A3=9E=E8=A6=81?= Date: Mon, 11 Dec 2023 01:36:02 +0000 Subject: [PATCH 16/24] update profiler/affinity_cpu_bind/bind_core.py. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 梅飞要 --- profiler/affinity_cpu_bind/bind_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/profiler/affinity_cpu_bind/bind_core.py b/profiler/affinity_cpu_bind/bind_core.py index 5c7a440384..510f325f48 100644 --- a/profiler/affinity_cpu_bind/bind_core.py +++ b/profiler/affinity_cpu_bind/bind_core.py @@ -152,7 +152,7 @@ def get_running_pids_on_npu(): RUNNINT_PIDs[id].append(pid) if RUNNINT_PIDs: return True - write_log_to_file('[WARNING] Find no running process on all NPUs, retry time {}, wait for 5 s'.format(times + 1)) + write_log_to_file('[WARNING] Find no running process on all NPUs, retry time {}, wait for 5s'.format(times + 1)) time.sleep(5) print('[WARNING] Find no running process on all NPUs, exit') return False -- Gitee From d4dd2117c9eb57b7d5534d3f9a2a47a2831aeff4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E9=A3=9E=E8=A6=81?= Date: Mon, 11 Dec 2023 02:00:48 +0000 Subject: [PATCH 17/24] update profiler/affinity_cpu_bind/README.md. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 梅飞要 --- profiler/affinity_cpu_bind/README.md | 31 ++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/profiler/affinity_cpu_bind/README.md b/profiler/affinity_cpu_bind/README.md index a2b865e14e..b7f9fa045b 100644 --- a/profiler/affinity_cpu_bind/README.md +++ b/profiler/affinity_cpu_bind/README.md @@ -4,15 +4,34 @@ 昇腾亲和性CPU绑核工具支持用户无需侵入式修改工程,直接运行工具即可实现按亲和性策略绑核,提升推理或训练性能。 ### **使用方式** -1. +1.命令行输入python3 bind_core.py -app/--application="inference/train cmd"(如果命令含多个参数,放在双引号中) +该方式会在拉起任务后,监测任务进程,并实施绑核,直至任务进程结束。 -2. +2.推理或训练任务已经拉起,命令行输入python3 bind_core.py。该方式会循环查找使用到NPU卡的任务进程,并实施绑核。 ### **使用须知 -1.使用工具前应提前安装pstree工具,参考命令yum install -y psmisc或apt -y install psmisc - -2.使用前手动执行npu-smi info -t topo,出现如下样例结果,说明环境支持绑核,否则请将环境驱动包升级到 - +1.该脚本会在拉起后查找使用到NPU卡的进程,每次查找10s,循环5次。如果找不到进程,会超时退出。 + +2.使用工具前应提前安装pstree工具,参考命令yum install -y psmisc或apt -y install psmisc。 + +3.使用前手动执行npu-smi info -t topo,出现如下样例结果,说明环境支持绑核,否则请将环境驱动包升级到。 + NPU0 NPU1 NPU2 NPU3 NPU4 NPU5 NPU6 NPU7 CPU Affinity +NPU0 X HCCS HCCS HCCS PHB SYS SYS SYS 144-167 +NPU1 HCCS X HCCS HCCS SYS PHB SYS SYS 96-119 +NPU2 HCCS HCCS X HCCS SYS SYS PHB SYS 48-71 +NPU3 HCCS HCCS HCCS X SYS SYS SYS PHB 0-23 +NPU4 PHB SYS SYS SYS X HCCS HCCS HCCS 144-167 +NPU5 SYS PHB SYS SYS HCCS X HCCS HCCS 96-119 +NPU6 SYS SYS PHB SYS HCCS HCCS X HCCS 48-71 +NPU7 SYS SYS SYS PHB HCCS HCCS HCCS X 0-23 + +Legend: + + X = Self + SYS = Path traversing PCIe and NUMA nodes. Nodes are connected through SMP, such as QPI, UPI. + PHB = Path traversing PCIe and the PCIe host bridge of a CPU. + HCCS = Connection traversing HCCS. + NA = Unknown relationship. -- Gitee From 483380c42209aaf21a6606769877d1f617845469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E9=A3=9E=E8=A6=81?= Date: Mon, 11 Dec 2023 02:01:06 +0000 Subject: [PATCH 18/24] update profiler/affinity_cpu_bind/README.md. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 梅飞要 --- profiler/affinity_cpu_bind/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/profiler/affinity_cpu_bind/README.md b/profiler/affinity_cpu_bind/README.md index b7f9fa045b..90c11d1a52 100644 --- a/profiler/affinity_cpu_bind/README.md +++ b/profiler/affinity_cpu_bind/README.md @@ -15,6 +15,7 @@ 2.使用工具前应提前安装pstree工具,参考命令yum install -y psmisc或apt -y install psmisc。 3.使用前手动执行npu-smi info -t topo,出现如下样例结果,说明环境支持绑核,否则请将环境驱动包升级到。 + NPU0 NPU1 NPU2 NPU3 NPU4 NPU5 NPU6 NPU7 CPU Affinity NPU0 X HCCS HCCS HCCS PHB SYS SYS SYS 144-167 NPU1 HCCS X HCCS HCCS SYS PHB SYS SYS 96-119 -- Gitee From 52089a8cfade3569a9620b3e4c506a031b55bb01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E9=A3=9E=E8=A6=81?= Date: Mon, 11 Dec 2023 02:01:37 +0000 Subject: [PATCH 19/24] update profiler/affinity_cpu_bind/README.md. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 梅飞要 --- profiler/affinity_cpu_bind/README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/profiler/affinity_cpu_bind/README.md b/profiler/affinity_cpu_bind/README.md index 90c11d1a52..c9d9f320d1 100644 --- a/profiler/affinity_cpu_bind/README.md +++ b/profiler/affinity_cpu_bind/README.md @@ -17,15 +17,24 @@ 3.使用前手动执行npu-smi info -t topo,出现如下样例结果,说明环境支持绑核,否则请将环境驱动包升级到。 NPU0 NPU1 NPU2 NPU3 NPU4 NPU5 NPU6 NPU7 CPU Affinity + NPU0 X HCCS HCCS HCCS PHB SYS SYS SYS 144-167 + NPU1 HCCS X HCCS HCCS SYS PHB SYS SYS 96-119 + NPU2 HCCS HCCS X HCCS SYS SYS PHB SYS 48-71 + NPU3 HCCS HCCS HCCS X SYS SYS SYS PHB 0-23 + NPU4 PHB SYS SYS SYS X HCCS HCCS HCCS 144-167 + NPU5 SYS PHB SYS SYS HCCS X HCCS HCCS 96-119 + NPU6 SYS SYS PHB SYS HCCS HCCS X HCCS 48-71 + NPU7 SYS SYS SYS PHB HCCS HCCS HCCS X 0-23 + Legend: X = Self -- Gitee From 9defef598a2ab92456d12ccfc00498f3377c53d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E9=A3=9E=E8=A6=81?= Date: Mon, 11 Dec 2023 02:02:22 +0000 Subject: [PATCH 20/24] update profiler/affinity_cpu_bind/README.md. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 梅飞要 --- profiler/affinity_cpu_bind/README.md | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/profiler/affinity_cpu_bind/README.md b/profiler/affinity_cpu_bind/README.md index c9d9f320d1..f57cef8378 100644 --- a/profiler/affinity_cpu_bind/README.md +++ b/profiler/affinity_cpu_bind/README.md @@ -16,23 +16,16 @@ 3.使用前手动执行npu-smi info -t topo,出现如下样例结果,说明环境支持绑核,否则请将环境驱动包升级到。 - NPU0 NPU1 NPU2 NPU3 NPU4 NPU5 NPU6 NPU7 CPU Affinity -NPU0 X HCCS HCCS HCCS PHB SYS SYS SYS 144-167 - -NPU1 HCCS X HCCS HCCS SYS PHB SYS SYS 96-119 - -NPU2 HCCS HCCS X HCCS SYS SYS PHB SYS 48-71 - -NPU3 HCCS HCCS HCCS X SYS SYS SYS PHB 0-23 - -NPU4 PHB SYS SYS SYS X HCCS HCCS HCCS 144-167 - -NPU5 SYS PHB SYS SYS HCCS X HCCS HCCS 96-119 - -NPU6 SYS SYS PHB SYS HCCS HCCS X HCCS 48-71 - -NPU7 SYS SYS SYS PHB HCCS HCCS HCCS X 0-23 +- NPU0 NPU1 NPU2 NPU3 NPU4 NPU5 NPU6 NPU7 CPU Affinity +- NPU0 X HCCS HCCS HCCS PHB SYS SYS SYS 144-167 +- NPU1 HCCS X HCCS HCCS SYS PHB SYS SYS 96-119 +- NPU2 HCCS HCCS X HCCS SYS SYS PHB SYS 48-71 +- NPU3 HCCS HCCS HCCS X SYS SYS SYS PHB 0-23 +- NPU4 PHB SYS SYS SYS X HCCS HCCS HCCS 144-167 +- NPU5 SYS PHB SYS SYS HCCS X HCCS HCCS 96-119 +- NPU6 SYS SYS PHB SYS HCCS HCCS X HCCS 48-71 +- NPU7 SYS SYS SYS PHB HCCS HCCS HCCS X 0-23 Legend: -- Gitee From 02b333797b056650f004e02b7f6a11ad91d49567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E9=A3=9E=E8=A6=81?= Date: Mon, 11 Dec 2023 02:03:54 +0000 Subject: [PATCH 21/24] update profiler/affinity_cpu_bind/README.md. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 梅飞要 --- profiler/affinity_cpu_bind/README.md | 25 +------------------------ profiler/affinity_cpu_bind/image.png | Bin 0 -> 77897 bytes 2 files changed, 1 insertion(+), 24 deletions(-) create mode 100644 profiler/affinity_cpu_bind/image.png diff --git a/profiler/affinity_cpu_bind/README.md b/profiler/affinity_cpu_bind/README.md index f57cef8378..8b8ee9ebf5 100644 --- a/profiler/affinity_cpu_bind/README.md +++ b/profiler/affinity_cpu_bind/README.md @@ -16,30 +16,7 @@ 3.使用前手动执行npu-smi info -t topo,出现如下样例结果,说明环境支持绑核,否则请将环境驱动包升级到。 - -- NPU0 NPU1 NPU2 NPU3 NPU4 NPU5 NPU6 NPU7 CPU Affinity -- NPU0 X HCCS HCCS HCCS PHB SYS SYS SYS 144-167 -- NPU1 HCCS X HCCS HCCS SYS PHB SYS SYS 96-119 -- NPU2 HCCS HCCS X HCCS SYS SYS PHB SYS 48-71 -- NPU3 HCCS HCCS HCCS X SYS SYS SYS PHB 0-23 -- NPU4 PHB SYS SYS SYS X HCCS HCCS HCCS 144-167 -- NPU5 SYS PHB SYS SYS HCCS X HCCS HCCS 96-119 -- NPU6 SYS SYS PHB SYS HCCS HCCS X HCCS 48-71 -- NPU7 SYS SYS SYS PHB HCCS HCCS HCCS X 0-23 - - -Legend: - - X = Self - SYS = Path traversing PCIe and NUMA nodes. Nodes are connected through SMP, such as QPI, UPI. - PHB = Path traversing PCIe and the PCIe host bridge of a CPU. - HCCS = Connection traversing HCCS. - NA = Unknown relationship. - - - - - +![输入图片说明](image.png) diff --git a/profiler/affinity_cpu_bind/image.png b/profiler/affinity_cpu_bind/image.png new file mode 100644 index 0000000000000000000000000000000000000000..09db6251f28ae8ebaf2bec07b75f526d040f9b85 GIT binary patch literal 77897 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 p0h?z~zMlTw4dJf_O&Jf4Yqop8eBd$j^9cP Date: Mon, 11 Dec 2023 02:25:05 +0000 Subject: [PATCH 22/24] update profiler/affinity_cpu_bind/README.md. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 梅飞要 --- profiler/affinity_cpu_bind/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/profiler/affinity_cpu_bind/README.md b/profiler/affinity_cpu_bind/README.md index 8b8ee9ebf5..f51b6c21f8 100644 --- a/profiler/affinity_cpu_bind/README.md +++ b/profiler/affinity_cpu_bind/README.md @@ -14,7 +14,7 @@ 2.使用工具前应提前安装pstree工具,参考命令yum install -y psmisc或apt -y install psmisc。 -3.使用前手动执行npu-smi info -t topo,出现如下样例结果,说明环境支持绑核,否则请将环境驱动包升级到。 +3.使用前手动执行npu-smi info -t topo,出现如下样例结果,说明环境支持绑核,否则请将环境驱动包升级到Ascend HDK 23.0.RC2以上版本。 ![输入图片说明](image.png) -- Gitee From 3424b5ec70bf8b1c5dce40bf82d0b5770f225a9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E9=A3=9E=E8=A6=81?= Date: Tue, 23 Jan 2024 02:51:20 +0000 Subject: [PATCH 23/24] update profiler/affinity_cpu_bind/bind_core.py. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 梅飞要 --- profiler/affinity_cpu_bind/bind_core.py | 373 ++++++++++++++---------- 1 file changed, 215 insertions(+), 158 deletions(-) diff --git a/profiler/affinity_cpu_bind/bind_core.py b/profiler/affinity_cpu_bind/bind_core.py index 510f325f48..9f6f2a02c5 100644 --- a/profiler/affinity_cpu_bind/bind_core.py +++ b/profiler/affinity_cpu_bind/bind_core.py @@ -13,180 +13,237 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os +import subprocess import re -import pwd -import sys -import time -import signal import argparse +import os import datetime -import subprocess +import sys +# import signal +import time + +NUP_IDS = [] +RUNNING_PIDS = {} +NPU_CPU_DICT = {} +BIND_CORES_WAIT_TIME = 0 +RUNNING_USER_NAME = '' + +# signal.signal(signal.SIGCHLD, signal.SIG_IGN) + +# binding core log file +nowtime = datetime.datetime.now() +BIND_CORE_RESULT_FILE = 'bind_core_' + \ + str(nowtime.year) + '_' + \ + str(nowtime.month) + '_' + \ + str(nowtime.day) + '_' + \ + str(nowtime.hour) + '_' + \ + str(nowtime.minute) + '_' + \ + str(nowtime.second) + '.txt' +tmp = sys.stdout -NPU_IDs = [] -RUNNINT_PIDs = {} -NPU_AFFINITY_CPUs = {} -CURRENT_USER = '' -BIND_CORE_RESULT_FILE = '' - -signal.signal(signal.SIGCHLD, signal.SIG_IGN) - -def create_log_file(): - global BIND_CORE_RESULT_FILE - time = datetime.datetime.now() - BIND_CORE_RESULT_FILE = 'bind_core_' + \ - str(time.year) + '_' + \ - str(time.month) + '_' + \ - str(time.day) + '_' + \ - str(time.hour) + '_' + \ - str(time.minute) + '_' + \ - str(time.second) + '_' + '.txt' - -def write_log_to_file(str): - global BIND_CORE_RESULT_FILE - file = open(BIND_CORE_RESULT_FILE, 'w') - file.write(str) - file.close() - -def launch_cmd(cmd): - write_log_to_file('[INFO] Start to launch cmd: {}'.format(cmd)) +PRINT_FILE = open(BIND_CORE_RESULT_FILE, 'w') + +# get process user +def get_process_user(pid): + cmd = 'ps -ef | grep {}'.format(pid) + p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + res = p.communicate()[0].decode('utf-8').strip().split() + return res[0] + +# print log to stdout +def print_log_to_stdout_and_exit(msg): + sys.stdout = tmp + print(msg) + exit() + +# print log to logfile +def print_log_to_file(msg): + sys.stdout = PRINT_FILE + print(msg) + sys.stdout = tmp + +# launch training or inference process +def launch_process(cmd): + global RUNNING_CMD_PID + print_log_to_file('[INFO] Start to execute cmd: {}'.format(cmd)) subprocess.Popen(cmd, shell=True) - write_log_to_file('[INFO] Succeed to launch cmd: {}.'.format(cmd)) +# parse input cmd def args_parse(): - parser = argparse.ArgumentParser(description='Description: Ascend Affinity-CPU-Cores Binding Script.') - parser.add_argument('-app', '--application', metavar='', nargs='+', help='Training or inference project command that you want to run. If there are more than one argument, write them in \" \".') + global BIND_CORES_WAIT_TIME + parser = argparse.ArgumentParser(description='This is a sample program.') + parser.add_argument('-t', '--time', type=int, metavar='', nargs='+', help='Wait time before bind cores that you want to set. |nThe unit is \'s\'') + parser.add_argument('-app', '--application', metavar='', nargs='+', help='Training or inference command that you want to run.\" \"') args = parser.parse_args() if args.application: application_cmd = ' '.join(args.application) - launch_cmd(application_cmd) - - -def get_current_user(): - global CURRENT_USER - CURRENT_USER = os.getuid() - -def subprocess_cmd(cmd): - res = '' - try: - res = subprocess.getoutput(cmd) - if re.search("Traceback", res): - write_log_to_file('[ERROR] Failed to run cmd: {}.'.format(cmd)) - except(Exception, TimeoutError) as err: - write_log_to_file('[ERROR] Failed to run cmd: {}.'.format(cmd)) - finally: - pass - return res - -def get_total_npu_ids(): - global NPU_IDs - - NPU_IDs = re.findall(r'\d+', subprocess_cmd('npu-smi info -l | grep "NPU ID"')) - if not NPU_IDs: - write_log_to_file('[ERROR] Failed to get npu ids on this device, please check!') - exit() - NPU_IDs = list(map(int, NPU_IDs)) - write_log_to_file('[INFO] Total npu ids on this device {}'.format(NPU_IDs)) - -def get_npu_affinity_cpus(): - global NPU_AFFINITY_CPUs - global NPU_IDs - - cpu_nums = re.findall(r'\d+', subprocess_cmd('lscpu | grep "CPU(s)"')) - if not cpu_nums: - write_log_to_file('[ERROR] Failed to get cpu nums on this device, please check!') - exit() - total_cpu_num = int(cpu_nums[0]) - node_nums = re.findall(r'\d+', subprocess_cmd('lscpu | grep "node(s)"')) - if not node_nums: - write_log_to_file('[ERROR] Failed to get node nums on this device, please check!') - exit() - total_node_num = int(node_nums[0]) - cpu_compensating = int(total_cpu_num / total_node_num) - - npu_topo_info = re.findall(r'\d+-\d+', subprocess_cmd('npu-smi info -t topo')) - if not npu_topo_info: - write_log_to_file('[ERROR] Failed to get npu topo info on this device, please check!') - exit() - - npu_index = 0 - for info in npu_topo_info: - cpu_list = info.split('-') - NPU_AFFINITY_CPUs[NPU_IDs[npu_index]] = cpu_list[0] + '-' + str(int(cpu_list[1]) + cpu_compensating) - npu_index += 1 - for k in NPU_AFFINITY_CPUs.keys(): - write_log_to_file('[INFO] Affinity CPU list {} for NPU {}'.format(NPU_AFFINITY_CPUs[k], k)) - -# get npu list and affinity CPU info -def get_current_dev_info(): - get_total_npu_ids() - get_npu_affinity_cpus() - -def get_process_user(pid): - try: - stat = os.stat(f'/proc/{pid}') - return stat.st_uid - except FileNotFoundError: - return None - -#get running pid on npu -def get_running_pids_on_npu(): - global RUNNINT_PIDs - global NPU_IDs - write_log_to_file('[INFO] Start to find running pids on all NPUs') - RUNNINT_PIDs.clear() - + launch_process(application_cmd) + time.sleep(10) + if args.time: + BIND_CORES_WAIT_TIME = int(args.time[0]) + + # if time is set, wait for setting time before bind cores + if BIND_CORES_WAIT_TIME != 0: + time.sleep(BIND_CORES_WAIT_TIME) + +# get npu affinity +def get_npu_affinity(): + global NPU_CPU_DICT + global NUP_IDS + + cmd = 'uname -m' + p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + res = p.communicate()[0].decode('utf-8').strip().split() + cpu_compensating = 0 + + # arm: need cpu compensating + if res[0] == 'aarch64': + cmd = 'lscpu | grep "CPU:"' + p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + res = p.communicate()[0].decode('utf-8').strip().split() + cpu_num = int(res[1]) + cmd = 'lscpu | grep "NUMA"' + p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + res = p.communicate()[0].decode('utf-8').strip().split() + node_num = int(res[2]) + print(node_num) + + cpu_compensating = int(cpu_num / node_num) + + cmd = 'npu-smi info -t topo' + p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + res = p.communicate()[0].decode('utf-8').strip().split() + + i = 0 + for v in res: + if '-' in v: + cpu_list = v.split('-') + v = cpu_list[0] + '-' + str(int(cpu_list[1]) + cpu_compensating) + NPU_CPU_DICT[NUP_IDS[i]] = v + i += 1 + for k in NPU_CPU_DICT.keys(): + print_log_to_file('[INFO] Affinity CPU list {} for NPU {}'.format(NPU_CPU_DICT[k], k)) + +# get total npu id +def get_total_npu_id(): + global NUP_IDS + cmd = 'npu-smi info -l | grep "NPU ID"' + p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + res = p.communicate()[0].decode('utf-8').strip().split() + for i in res: + if i.isdigit(): + NUP_IDS.append(int(i)) + if not NUP_IDS: + print_log_to_stdout_and_exit('[ERROR] Failed to get total NPU id list, please make sure there is NPU on this device') + print_log_to_file('[INFO] NPU total id list: {}'.format(NUP_IDS)) + +# get app pid on npu +def get_pid_on_npu(): + global RUNNING_PIDS + global NUP_IDS + print_log_to_file('[INFO] Begin to find running process on all NPUs') + RUNNING_PIDS.clear() + # get process pid on NPUs, retry times : 5 for times in range(5): - for id in NPU_IDs: - pids = re.findall(r'id:\d+', subprocess_cmd('npu-smi info -t proc-mem -i {} -c 0'.format(id))) - if not pids: - continue - for pid in pids: - pid = int(pid.split(':')[1]) - if get_process_user(pid) != CURRENT_USER: - continue - - if id not in RUNNINT_PIDs: - RUNNINT_PIDs[id] = [ pid ] - else: - RUNNINT_PIDs[id].append(pid) - if RUNNINT_PIDs: - return True - write_log_to_file('[WARNING] Find no running process on all NPUs, retry time {}, wait for 5s'.format(times + 1)) + for i in NUP_IDS: + cmd = 'npu-smi info -t proc-mem -i {} -c 0'.format(int(i)) + p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + res = p.communicate()[0].decode('utf-8').strip().split() + + if 'Process' in res: + for v in res: + if v.startswith('id:'): + pid_on_npu = v.split(':')[1] + if get_process_user(int(pid_on_npu)) != RUNNING_USER_NAME: + continue + if i not in RUNNING_PIDS: + RUNNING_PIDS[i] = [int(pid_on_npu)] + else: + RUNNING_PIDS[i].append(int(pid_on_npu)) + + if RUNNING_PIDS: + break + print_log_to_file('[WARNING] Found no running process on all NPUs, retry times: {}, wait for 5 s'.format(times + 1)) + # wait 5 s for each time time.sleep(5) - print('[WARNING] Find no running process on all NPUs, exit') - return False -def run_bind_cpu_cores(): - global NPU_IDs - global NPU_AFFINITY_CPUs - for npu_id, pid_list in RUNNINT_PIDs.items(): - start_cpu_id, end_cpu_id = NPU_AFFINITY_CPUs[npu_id].split('-') + # no running process on NPUs, stop + if not RUNNING_PIDS: + print_log_to_file('[INFO] Found no running process on all NPUs, stop bind cores') + print_log_to_stdout_and_exit('[INFO] Now there is no running process on all NPUs, stop bind cores, you can see bind core results in ' + BIND_CORE_RESULT_FILE) + + # delete repeat pid + for i in NUP_IDS: + if i in RUNNING_PIDS: + pids_npu = RUNNING_PIDS[i] + for n, pid in RUNNING_PIDS.items(): + if n != i and pid in pids_npu: + RUNNING_PIDS[n].remove(pid) + + for k in RUNNING_PIDS.keys(): + print_log_to_file('[INFO] Succeed to find running process {} on NPU {}'.format(RUNNING_PIDS[k], k)) + +# get device info +def get_dev_info(): + get_total_npu_id() + get_npu_affinity() + +# get process affinity +def get_process_affinity(pid): + cmd = 'taskset -pc {} '.format(pid) + p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + res = p.communicate()[0].decode('utf-8').strip().split() + if res: + return res[len(res) - 1] + +# run bind core +def run_bind_core(): + global NUP_IDS + global NPU_CPU_DICT + for k, pid_list in RUNNING_PIDS.items(): + cpu_list = NPU_CPU_DICT[k].split('-') + start_cpu_id = cpu_list[0] + end_cpu_id = cpu_list[1] for pid in pid_list: - pid_tree = re.findall(r'(\d+)', subprocess_cmd('pstree {} -p -T'.format(pid))) - for pid in pid_tree: - current_affinity_cpu_info = subprocess_cmd('taskset -pc {}'.format(pid)) - if '-' in current_affinity_cpu_info: - current_start_cpu, current_end_cpu = current_affinity_cpu_info.split(' ')[-1].split('-') - if start_cpu_id == current_start_cpu and end_cpu_id == current_end_cpu: - continue - write_log_to_file('[INFO] Start to bind CPU cores for process {} on NPU {}'.format(pid, npu_id)) - res = subprocess_cmd('taskset -pc {}-{} {}'.format(start_cpu_id, end_cpu_id, pid)) - if 'failed' in res: - write_log_to_file('[WARNING] Failed to bind CPU cores for process {} on NPU {}'.format(pid, npu_id)) - write_log_to_file('[INFO] Succeed to bind CPU cores for process {} on NPU {} with CPU {}'.format(pid, npu_id, NPU_AFFINITY_CPUs[npu_id])) + cmd = 'pstree {} -p -T'.format(pid) + p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + res = p.communicate()[0].decode('utf-8').strip().split() + for ele in res: + ele = re.sub(u"\\(|\\)", ",", ele) + ele_list = ele.split(',') + for sub_p in ele_list: + if sub_p.isdigit(): + sub_p = int(sub_p) + + # if process has set to right affinity, continue + current_affinity_cpu_list = get_process_affinity(sub_p) + if not current_affinity_cpu_list: + continue + current_cpu_list = current_affinity_cpu_list.split('-') + if current_cpu_list and current_cpu_list[0] == start_cpu_id and current_cpu_list[1] == end_cpu_id: + continue + print_log_to_file('[INFO] Begin to bind cores for process {} on NPU {}'.format(str(sub_p), k)) + cmd = 'taskset -pc {}-{} {}'.format(int(start_cpu_id), int(end_cpu_id), sub_p) + p = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print_log_to_file(p.stdout.decode('utf-8')) + + print_log_to_file('[INFO] Succeed to bind process {} on NPU {} with cpu cores list {}'.format(str(sub_p), k, NPU_CPU_DICT[k])) + +# get current process user +def get_current_user(): + global RUNNING_USER_NAME + RUNNING_USER_NAME = get_process_user(os.getpid()) if __name__ == '__main__': - print('[INFO] Start to run Ascend_affinity_cpu_core_binding script...') - create_log_file() + print("[INFO] Begin to run bind-cores script...") args_parse() get_current_user() - get_current_dev_info() - - while(True): - ret = get_running_pids_on_npu() - if not ret: - exit() - run_bind_cpu_cores() - + get_dev_info() + + while True: + get_pid_on_npu() + run_bind_core() + + print("[INFO] Finished to bind cores, you can see bind core results in " + BIND_CORE_RESULT_FILE) + -- Gitee From 4d75e704c218a3aa31105b05a8cb2275d3c15960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E9=A3=9E=E8=A6=81?= Date: Tue, 23 Jan 2024 02:51:57 +0000 Subject: [PATCH 24/24] update profiler/affinity_cpu_bind/bind_core.py. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 梅飞要 --- profiler/affinity_cpu_bind/bind_core.py | 1 - 1 file changed, 1 deletion(-) diff --git a/profiler/affinity_cpu_bind/bind_core.py b/profiler/affinity_cpu_bind/bind_core.py index 9f6f2a02c5..46a7eb1b06 100644 --- a/profiler/affinity_cpu_bind/bind_core.py +++ b/profiler/affinity_cpu_bind/bind_core.py @@ -106,7 +106,6 @@ def get_npu_affinity(): p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) res = p.communicate()[0].decode('utf-8').strip().split() node_num = int(res[2]) - print(node_num) cpu_compensating = int(cpu_num / node_num) -- Gitee