已合并
test目录新增hccl_vm工具 #2925
lijuntao26创建于 6月22日
test目录新增hccl_vm工具 #2925
已合并
lijuntao26创建于 6月22日
744 个文件变更+175389-0
@@ -0,0 +1,19 @@
1+**/.idea/
2+**/.vscode/
3+**/cmake-build-debug/
4+**/cmake-build-release/
5+**/output/
6+**/build/
7+**/build_test/
8+**/tmp/
9+**/CMakeFiles/
10+**/_deps/
11+**/hccl_vm_install/
12+**/lib64/
13+**/hccl_device_install/
14+**/build_device/
15+**/node_modules/
16+**/dist/
17+coverage_report/
18+ut_logs/
19+third_party/
@@ -0,0 +1,147 @@
1+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
2+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
3+# CANN Open Software License Agreement Version 2.0 (the "License").
4+# Please refer to the License for details. You may not use this file except in compliance with the License.
5+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
6+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
7+# See LICENSE in the root of the software repository for the full text of the License.
8+ 
9+cmake_minimum_required(VERSION 3.10)
10+project(hccl_vm VERSION 1.0.0)
11+#set(CMAKE_VERBOSE_MAKEFILE true)
12+ 
13+set(CMAKE_CXX_STANDARD 17)
14+set(CMAKE_CXX_STANDARD_REQUIRED True)
15+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread")
16+set(CMAKE_POSITION_INDEPENDENT_CODE ON)
17+ 
18+set(OUTPUT_DIR ${CMAKE_BINARY_DIR}/output)
19+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${OUTPUT_DIR})
20+set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${OUTPUT_DIR})
21+set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${OUTPUT_DIR})
22+ 
23+option(ENABLE_COVERAGE "Enable gcov/lcov coverage instrumentation for all targets" OFF)
24+if(ENABLE_COVERAGE)
25+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage -fprofile-arcs -ftest-coverage")
26+ set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage")
27+ set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --coverage")
28+ message(STATUS "Coverage instrumentation enabled: --coverage (all targets)")
29+endif()
30+ 
31+option(BUILD_DEVICE_ARM "Build device_arm (ARM cross-compile)" OFF)
32+ 
33+ 
34+set(HCOMM_VM_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
35+ 
36+if(NOT DEFINED ASCEND_CANN_PACKAGE_PATH)
37+ if(NOT DEFINED ENV{ASCEND_TOOLKIT_HOME})
38+ message(FATAL_ERROR "Error: ASCEND_TOOLKIT_HOME environment variable is not set. Please set it to the Ascend Toolkit installation path.")
39+ endif()
40+ set(ASCEND_CANN_PACKAGE_PATH $ENV{ASCEND_TOOLKIT_HOME})
41+endif()
42+message(STATUS "ASCEND_CANN_PACKAGE_PATH: ${ASCEND_CANN_PACKAGE_PATH}")
43+ 
44+if(BUILD_DEVICE_ARM)
45+ if(NOT DEFINED HCCL_CODE_ROOT)
46+ if(NOT DEFINED ENV{HCCL_CODE_HOME})
47+ message(FATAL_ERROR "Error: BUILD_DEVICE_ARM is ON, but HCCL_CODE_HOME environment variable is not set. Please set it to the HCCL source path.")
48+ endif()
49+ set(HCCL_CODE_ROOT $ENV{HCCL_CODE_HOME})
50+ endif()
51+ message(STATUS "HCCL_CODE_ROOT: ${HCCL_CODE_ROOT}")
52+endif()
53+ 
54+if(NOT DEFINED HCOMM_CODE_ROOT)
55+ if(NOT DEFINED ENV{HCOMM_CODE_HOME})
56+ message(FATAL_ERROR "Error: HCOMM_CODE_HOME environment variable is not set. Please set it to the Ascend Toolkit installation path.")
57+ endif()
58+ set(HCOMM_CODE_ROOT $ENV{HCOMM_CODE_HOME})
59+endif()
60+message(STATUS "HCOMM_CODE_ROOT: ${HCOMM_CODE_ROOT}")
61+ 
62+set(HCCL_VM_INCLUDE ${CMAKE_SOURCE_DIR}/include)
63+set(HCCL_VM_SRC ${CMAKE_SOURCE_DIR}/src)
64+set(HCCL_VM_INSTALL ${CMAKE_SOURCE_DIR}/hccl_vm_install)
65+set(THRID_PARTY_DIR ${CMAKE_SOURCE_DIR}/third_party)
66+message(STATUS "HCOMM_CODE_ROOT: ${HCOMM_CODE_ROOT}")
67+ 
68+set(HCOMM_CODE_PLATFORM ${HCOMM_CODE_ROOT}/src/platform)
69+ 
70+# 使用一个自定义的 Cache 变量,默认值设为 OFF 或空
71+set(HCCL_VM_PROJECT_PREFIX_SET OFF CACHE INTERNAL "Check if install prefix is set")
72+ 
73+if(NOT HCCL_VM_PROJECT_PREFIX_SET)
74+ # 只要这个逻辑没跑成功过,就设置一次
75+ set(CMAKE_INSTALL_PREFIX "${CMAKE_SOURCE_DIR}/hccl_vm_install" CACHE PATH "Default install prefix" FORCE)
76+ set(HCCL_VM_PROJECT_PREFIX_SET ON CACHE INTERNAL "Check if install prefix is set")
77+ message(STATUS "First-time setup: Installation prefix locked to: ${CMAKE_INSTALL_PREFIX}")
78+endif()
79+ 
80+set(CMAKE_INSTALL_RPATH "\$ORIGIN")
81+ 
82+include(cmake/third_party/yaml-cpp.cmake)
83+include(cmake/third_party/nlohmann_json.cmake)
84+include(cmake/third_party/cli11.cmake)
85+include(cmake/third_party/spdlog.cmake)
86+include(cmake/third_party/sqlite3.cmake)
87+ 
88+function(config_hcclvm_install)
89+ install(DIRECTORY DESTINATION "data")
90+ install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/asset/"
91+ DESTINATION "."
92+ # 文件的权限 (755)
93+ FILE_PERMISSIONS
94+ OWNER_READ OWNER_WRITE OWNER_EXECUTE
95+ GROUP_READ GROUP_EXECUTE
96+ WORLD_READ WORLD_EXECUTE
97+ # 目录的权限 (755)
98+ DIRECTORY_PERMISSIONS
99+ OWNER_READ OWNER_WRITE OWNER_EXECUTE
100+ GROUP_READ GROUP_EXECUTE
101+ WORLD_READ WORLD_EXECUTE
102+ )
103+endfunction()
104+ 
105+# 编译HCCL-VM
106+if (NOT BUILD_DEVICE_ARM)
107+ config_hcclvm_install()
108+ add_subdirectory(src)
109+endif()
110+ 
111+set(CPACK_PACKAGE_NAME "hccl_vm")
112+set(CPACK_GENERATOR "TGZ")
113+include(CPack)
114+ 
115+# 编译DEVICE-ARM-交叉编译版本
116+if(BUILD_DEVICE_ARM)
117+ message(STATUS "BUILD_DEVICE_ARM start")
118+ set(DEVICE_ARM_BUILD_DIR ${CMAKE_SOURCE_DIR}/build_device)
119+ set(DEVICE_ARM_INSTALL_DIR ${CMAKE_SOURCE_DIR}/hccl_vm_install/device)
120+ include(ExternalProject)
121+ ExternalProject_Add(device_arm_external
122+ SOURCE_DIR ${CMAKE_SOURCE_DIR}/src/device_arm
123+ CMAKE_ARGS
124+ -DCMAKE_INSTALL_PREFIX=${DEVICE_ARM_INSTALL_DIR}
125+ -DHCCL_VM_SRC=${HCCL_VM_SRC}
126+ -DTHRID_PARTY_DIR=${THRID_PARTY_DIR}
127+ -DHCCL_VM_INCLUDE=${HCCL_VM_INCLUDE}
128+ -DASCEND_CANN_PACKAGE_PATH=${ASCEND_CANN_PACKAGE_PATH}
129+ -DHCOMM_CODE_ROOT=${HCOMM_CODE_ROOT}
130+ -DHCCL_CODE_ROOT=${HCCL_CODE_ROOT}
131+
132+ BINARY_DIR ${DEVICE_ARM_BUILD_DIR}
133+ INSTALL_DIR ${DEVICE_ARM_INSTALL_DIR}
134+ BUILD_ALWAYS ON
135+ )
136+
137+ message(STATUS "device_arm cross-compilation enabled")
138+ message(STATUS " Build dir: ${DEVICE_ARM_BUILD_DIR}")
139+ message(STATUS " Install dir: ${DEVICE_ARM_INSTALL_DIR}")
140+endif()
141+ 
142+ 
143+option(BUILD_TESTS "Build unit tests" OFF)
144+if(BUILD_TESTS)
145+ include(cmake/third_party/gtest.cmake)
146+ add_subdirectory(test)
147+endif()
@@ -0,0 +1,745 @@
1+# HCCL-VM 指导手册
2+ 
3+## 1. 概述
4+ 
5+HCCL-VM是面向华为昇腾NPU卡的高性能集合通信的虚拟执行环境,该工具旨在无真实昇腾硬件的条件下,实现HCCL集合通信算子的开发和功能验证。
6+ 
7+## 2. 前置依赖
8+ 
9+| | |
10+| -------- | ------------- |
11+| 系统架构 | x86_64 Ubuntu22.04及以上 |
12+| 规格约束 | Ascend950,其余参照[约束详情](#45-工具规格约束) |
13+ 
14+### 2.1 CANN包安装
15+ 
16+安装最新版本CANN Toolkit开发套件包和CANN ops算子包 [下载链接](https://ascend.devcloud.huaweicloud.com/artifactory/cann-run-mirror/software/master/)
17+ 
18+```bash
19+./Ascend-cann-toolkit_9.1.0_linux-x86_64.run --install --install-path=/home/Ascend
20+./Ascend-cann-950-ops_9.1.0_linux-x86_64.run --install --install-path=/home/Ascend
21+```
22+ 
23+### 2.2 hccl_test编译
24+ 
25+hccl_test是昇腾官方提供的HCCL性能测试工具,详见[HCCL性能测试工具](https://www.hiascend.com/document/detail/zh/CANNCommunityEdition/910beta1/devaids/hccltool/HCCLpertest_16_0001.html),HCCL-VM支持在虚拟环境中运行hccl_test用例。请先参照[hccl_test用例构建](#42-hccl-test用例构建)章节进行用例二进制程序的编译。
26+ 
27+备注:可选,未来支持Pytorch用例
28+ 
29+---
30+ 
31+## 3. 快速上手
32+ 
33+### 3.1 工具构建&安装
34+ 
35+```bash
36+# 1. 创建工作目录
37+mkdir -p /home/workspace
38+cd /home/workspace
39+ 
40+# 2. 下载依赖源码
41+git clone https://gitcode.com/cann/hccl.git
42+git clone https://gitcode.com/cann/hcomm.git
43+git clone https://gitcode.com/zhupc158/CheckerL2.git
44+ 
45+# 3. 安装第三方依赖
46+sudo apt-get update
47+sudo apt install build-essential cmake libsqlite3-dev rdma-core libibverbs-dev pkg-config gcc-aarch64-linux-gnu g++-aarch64-linux-gnu qemu-user-static binfmt-support
48+ 
49+# 4. 编译HCCL-VM工具
50+source /home/Ascend/cann/set_env.sh
51+export HCCL_CODE_HOME=/home/workspace/hccl
52+export HCOMM_CODE_HOME=/home/workspace/hcomm
53+./build.sh --full
54+```
55+ 
56+### 3.2 使用示例
57+ 
58+#### 3.2.1 环境配置
59+ 
60+请参照[hccl_rootinfo文件内容](#47-hccl_rootinfojson文件),创建并配置hccl_rootinfo.json文件,
61+ 
62+#### 3.2.2 CCU模式
63+ 
64+1. 环境变量配置
65+ 
66+```bash
67+# 进入工具安装目录
68+cd /home/workspace/CheckerL2/hccl_vm_install
69+source /home/Ascend/cann/set_env.sh
70+export LD_LIBRARY_PATH=$ASCEND_HOME_PATH/lib64:$ASCEND_HOME_PATH/devlib:$LD_LIBRARY_PATH
71+export RANK_TABLE_FILE=$(pwd)/ranktable.json
72+export HCCL_OP_EXPANSION_MODE="CCU_SCHED"
73+```
74+ 
75+2. 执行
76+ 
77+```bash
78+# 进入工具安装目录
79+cd /home/workspace/CheckerL2/hccl_vm_install
80+ 
81+# 选择昇腾集群拓扑配置文件,启动工具,初始化集群环境,进入工具命令行
82+./hccl-vm start ascend950_cluster_32_server_normal.yaml
83+ 
84+# 如需启用runner插件(可选)
85+(hvm)$> hccl-vm plugin install @runner
86+ 
87+# 选择本次算子执行的通信域配置文件(在1个超节点1个Server1个NPU的集群环境运行hccl_test用例)
88+(hvm)$> hccl-vm mock-comm 112
89+(hvm)$> mpirun --allow-run-as-root --oversubscribe -np 2 ${ASCEND_HOME_PATH}/tools/hccl_test/bin/reduce_scatter_test -b 64 -e 64 -d int32 -o sum -w 0 -n 1 -c 1 > log.txt
90+ 
91+# 执行checker校验
92+(hvm)$> hccl-vm plugin run @checker
93+ 
94+# 退出工具终端
95+(hvm)$> exit
96+```
97+ 
98+3. 验证hccl_test用例运行结果 [Runner结果查看](#491-runner插件结果) [Checker结果查看](#492-checker插件结果)
99+ 
100+#### 3.2.3 AICPU模式
101+ 
102+AICPU展开模式需要将算法展开步骤放到设备侧执行,因此hccl-vm工具需要将HCCL的设备侧的符号编译并模拟执行。由于设备侧符号是ARM架构的,因此在X86环境上编译时需要借助交叉编译器,运行时需要借助QEMU实现AICPU模式的模拟运行
103+ 
104+设备侧符号使用hccl和hcomm的源码编译,为了保证Host与Device通信协议正确,需要同时编译Host侧的安装包并进行替换安装
105+ 
106+1. HCCL设备侧符号编译、安装、拷贝等
107+ 
108+```bash
109+cd /home/workspace/CheckerL2
110+bash ./build_pkg.sh
111+```
112+ 
113+2. 环境变量配置
114+ 
115+```bash
116+# 进入工具安装目录
117+cd /home/workspace/CheckerL2/hccl_vm_install
118+source /home/Ascend/cann/set_env.sh
119+export LD_LIBRARY_PATH=$ASCEND_HOME_PATH/lib64:$ASCEND_HOME_PATH/devlib:$LD_LIBRARY_PATH
120+export RANK_TABLE_FILE=$(pwd)/ranktable.json
121+export HCCL_OP_EXPANSION_MODE="AI_CPU"
122+```
123+ 
124+3. 执行
125+ 
126+```bash
127+# 进入工具安装目录
128+cd /home/workspace/CheckerL2/hccl_vm_install
129+ 
130+# 选择昇腾集群拓扑配置文件,启动工具,初始化集群环境,进入工具命令行
131+./hccl-vm start ascend950_cluster_32_server_normal.yaml
132+ 
133+# 如需启用runner插件(可选)
134+(hvm)$> hccl-vm plugin install @runner
135+ 
136+# 选择本次算子执行的通信域配置文件(在1个超节点1个Server1个NPU的集群环境运行hccl_test用例)
137+(hvm)$> hccl-vm mock-comm 112
138+(hvm)$> mpirun --allow-run-as-root --oversubscribe -np 2 ${ASCEND_HOME_PATH}/tools/hccl_test/bin/reduce_scatter_test -b 64 -e 64 -d int32 -o sum -w 0 -n 1 -c 1 > log.txt
139+ 
140+# 执行checker校验
141+(hvm)$> hccl-vm plugin run @checker
142+ 
143+# 退出工具终端
144+(hvm)$> exit
145+```
146+ 
147+4. 验证hccl_test用例运行结果 [Runner结果查看](#491-runner插件结果) [Checker结果查看](#492-checker插件结果)
148+ 
149+#### 3.2.4 AIV模式
150+ 
151+**当前仅支持Runner插件**
152+ 
153+1. 环境变量配置
154+ 
155+```bash
156+# 进入工具安装目录
157+cd /home/workspace/CheckerL2/hccl_vm_install
158+source /home/Ascend/cann/set_env.sh
159+export LD_LIBRARY_PATH=$ASCEND_HOME_PATH/lib64:$ASCEND_HOME_PATH/devlib:$LD_LIBRARY_PATH
160+export RANK_TABLE_FILE=$(pwd)/ranktable.json
161+export HCCL_OP_EXPANSION_MODE="AIV"
162+```
163+ 
164+2. 执行
165+ 
166+```bash
167+# 进入工具安装目录
168+cd /home/workspace/CheckerL2/hccl_vm_install
169+ 
170+# 选择昇腾集群拓扑配置文件,启动工具,初始化集群环境,进入工具命令行
171+./hccl-vm start ascend950_cluster_32_server_normal.yaml
172+ 
173+# 如需启用runner插件(可选)
174+(hvm)$> hccl-vm plugin install @runner
175+ 
176+# 选择本次算子执行的通信域配置文件(在1个超节点1个Server1个NPU的集群环境运行hccl_test用例)
177+(hvm)$> hccl-vm mock-comm 112
178+(hvm)$> mpirun --allow-run-as-root --oversubscribe -np 2 ${ASCEND_HOME_PATH}/tools/hccl_test/bin/reduce_scatter_test -b 64 -e 64 -d int32 -o sum -w 0 -n 1 -c 1 > log.txt
179+ 
180+# 退出工具终端
181+(hvm)$> exit
182+```
183+ 
184+3. 验证hccl_test用例运行结果 [Runner结果查看](#491-runner插件结果)
185+ 
186+### 3.3 Pytorch用例示例
187+ 
188+暂不支持
189+ 
190+### 3.4 hccl代码修改验证示例
191+ 
192+若您修改了CANN的算子包代码,如新增了算法类型,为保证您的修改生效,需要按照如下步骤操作执行。build_pkg.sh脚本帮助用户执行编包、装包、拷贝Device侧依赖符号,执行前需设置环境变量:
193+ 
194+```bash
195+# 假设您的CANN安装目录为:/home/Ascend
196+source /home/Ascend/cann/set_env.sh
197+# 配置hccl代码仓路径
198+export HCCL_CODE_HOME=/home/workspace/hccl
199+# 配置hcomm代码仓路径
200+export HCOMM_CODE_HOME=/home/workspace/hcomm
201+```
202+ 
203+1. 若您修改了CANN hccl仓代码,请执行bash build_pkg.sh --install hccl
204+2. 若您修改了CANN hcomm仓代码,请执行bash build_pkg.sh --install hcomm
205+3. 参考[使用示例](#32-使用示例)步骤,重新运行用例。
206+ 
207+---
208+ 
209+## 4. 详细指导
210+ 
211+### 4.1 工具环境变量配置
212+ 
213+**HCCL-VM环境变量说明**
214+ 
215+| 环境变量 | 用途 | 示例 |
216+| ------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------- |
217+| `HCCL_CODE_HOME` | HCCL-VM编译指定HCCL源码路径。默认未配置。 | `export HCCL_CODE_HOME=/home/workspace/hccl` |
218+| `HCOMM_CODE_HOME` | HCCL-VM编译指定HCOMM源码路径。默认未配置。 | `export HCOMM_CODE_HOME=/home/workspace/hcomm` |
219+| `HCCLVM_ENABLE_DUMP_DATA` | 使能Runner插件dump input\&output数据。若使能,则在测试用例执行过程中,会将每个算子的input\&output数据dump到all\_rank\_input\_output.txt文件中 | `export HCCLVM_ENABLE_DUMP_DATA=1 使能,export HCCLVM_ENABLE_DUMP_DATA=0 禁用` |
220+ 
221+### 4.2 HCCL-Test用例构建
222+ 
223+hccl_test用例源码在CANN包安装目录下,支持OpenMPI和MPICH两种环境编译、运行,运行时差异详见[OpenMPI和MPICH环境用例执行差异](#48-openmpi和mpich环境用例运行差异),本用例指导中以OpenMPI环境为例。
224+ 
225+#### 4.2.1 OpenMPI环境编译
226+ 
227+1. 安装OpenMPI
228+ 
229+```bash
230+sudo apt-get update
231+sudo apt install openmpi-bin libopenmpi-dev
232+```
233+ 
234+2. 编译hccl_test
235+ 
236+```bash
237+# 修改CANN安装目录权限
238+chmod -R 755 /home/Ascend
239+ 
240+# 进入hccl_test用例源码目录
241+cd /home/Ascend/cann/tools/hccl_test
242+ 
243+# 设置CANN环境变量
244+source /home/Ascend/cann/set_env.sh
245+ 
246+# 临时修改Makefile脚本
247+if ! grep -q '\-lmpi_cxx' Makefile; then
248+ sed -i 's/-lmpi/-lmpi -lmpi_cxx/g' Makefile
249+fi
250+ 
251+# 编译hccl_test用例
252+MPI_HOME=/usr/lib/x86_64-linux-gnu/openmpi make ASCEND_DIR=${ASCEND_HOME_PATH}
253+```
254+ 
255+#### 4.2.2 MPICH环境编译
256+ 
257+假设mpich的路径为: `/usr/lib/mpich`
258+ 
259+```bash
260+# 进入hccl_test用例源码目录
261+cd /home/Ascend/cann/tools/hccl_test
262+ 
263+# 设置CANN环境变量
264+source /home/Ascend/cann/set_env.sh
265+ 
266+# 配置环境变量
267+export LD_LIBRARY_PATH=/usr/lib/mpich/lib/:${ASCEND_HOME_PATH}/lib64/:${ASCEND_HOME_PATH}/x86_64-linux/devlib:$LD_LIBRARY_PATH
268+ 
269+# 编译hccl_test用例
270+make MPI_HOME=/usr/lib/mpich/ ASCEND_DIR=${ASCEND_HOME_PATH}
271+```
272+ 
273+### 4.3 昇腾集群拓扑配置文件说明
274+ 
275+#### 4.3.1 Server/Pod拓扑配置文件说明
276+ 
277+昇腾集群拓扑是由一个或多个Server/Pod形态的子拓扑,按照CLOS分层网络规则组合而成的。因此用户在生成集群拓扑前,需要确认每个Server/Pod的拓扑类型。
278+用户既可以选择HCCL-VM工具提供的预定义拓扑类型,也可以根据配置文件格式要求,自定义Server/Pod的拓扑类型。
279+ 
280+描述一个Server/Pod的拓扑网络关系,主要包括以下方面:
281+ 
282+ - **端口配置表**: 描述一个NPU卡的物理端口配置信息,如NPU与NPU直连端口(P2P),NPU出框端口(P2NET)等。
283+ - **链路配置表**: 描述一个Server/Pod内的所有NPU卡之间的连接关系,如全连接(Full Mesh)等。
284+ - **PortBound**: 描述一个NPU卡的某些端口的绑定关系,即多个端口绑定成一个PortGroup。
285+ 
286+```yaml
287+type: "server_intra_links"
288+name: "ascend950_links_topo_demo"
289+description: "ascend950芯片普通拓扑连接关系描述文件"
290+ 
291+soc_version: "Ascend950"
292+device_num: 16
293+ 
294+device_ports_allocate_map:
295+ # port分配表: 0: 不使用, 1: device直连, 2: device连交换机, 3: d2h端口
296+ # portId: 0 1 2 3 4 5 6 7 8
297+ - {die_id: 0, pin_map: [1, 1, 1, 0, 2, 2, 2, 2, 3]} # die0
298+ - {die_id: 1, pin_map: [0, 0, 0, 0, 0, 0, 0, 0, 0]} # die1
299+ 
300+# port_group: 描述哪些port合并为一个portGroup,相同portGroup的port对于IP地址相同
301+port_group:
302+ - {layer: 0, ports: ["0/4", "0/5", "0/6", "0/7"]}
303+ 
304+links:
305+ # ── 描述法:每行8个device组成全互联 ──
306+ - link_mode: "fullmesh"
307+ connections:
308+ # 如下示例表示:die0的device 0, 1, 2, 3都通过die0的port进行全连接
309+ - {die_id: 0, devices_range: [0, 3]}
310+ - {die_id: 0, devices_range: [4, 7]}
311+ - {die_id: 0, devices_range: [8, 11]}
312+ - {die_id: 0, devices_range: [12, 15]}
313+
314+ #- link_mode: "enum"
315+ # device_to_device_links:
316+ # 如下示例表示:device 0和1都通过die0的port,分别与device 1, 3, 5, 7的die1的port进行连接
317+ # 即:device0与device1, device3, device5, device7进行连接,device1与device3, device5, device7进行连接
318+ # - {src_die_id: 1, src_local_id_range: [0, 2], dst_die_id: 1, dst_local_id_range: [1, 3, 5, 7]}
319+ 
320+ - link_mode: "enum"
321+ device_to_switch_links:
322+ # 如下示例表示:device0到device15都通过die0的port连接到交换机。结合portGroup可知,device0到device15都通过portGroup[0/4, 0/5, 0/6, 0/7]连接到交换机。
323+ - {die_id: 0, devices_range: [0, 15]}
324+ 
325+```
326+ 
327+**字段说明**
328+ 
329+ - **soc_version**: 芯片型号,如 `Ascend950`。
330+ - **device_num**: 设备总数,根据芯片型号和拓扑类型确定。
331+ - **device_ports_allocate_map**: 端口分配表,描述每个die的端口配置。1表示device直连端口,2表示device连交换机端口,3表示d2h端口。
332+ - **port_group**: 描述哪些port合并为一个portGroup,相同portGroup的port对应IP地址相同。没有配置的port,则默认每个port为一个portGroup。
333+ - **links**: 链路配置表,描述Server/Pod内的所有NPU卡之间,以及NPU与交换机之间的连接关系。
334+ - **NPU直连关系**: 工具提供了两种方式配置NPU直连关系:
335+ - **link_mode == "fullmesh"**: 表示所有Device基于一个Die的Port进行全连接。后续有新增典型的连接方式,可以新增link_mode类型,如"ring"。
336+ - **link_mode == "enum"**: 枚举法。当Server/Pod内的NPU连接方式比较复杂时,可以通过枚举所有的链路关系来描述。
337+ - **NPU与交换机连接关系**: 用户可以通过枚举法配置NPU与交换机之间的连接关系。
338+ - **device_to_device_links**: 描述NPU与NPU之间的连接关系。
339+ - **device_to_switch_links**: 描述NPU与交换机之间的连接关系。
340+ 
341+#### 4.3.2 集群拓扑配置文件说明
342+ 
343+昇腾集群网络是由一个或多个Server/Pod形态的子拓扑,按照CLOS分层网络规则组合而成的。用户可根据集群规模和需求,选择不同的Server/Pod拓扑类型。
344+ 
345+用户可按照如下配置文件格式,自定义集群拓扑配置文件:
346+ 
347+```yaml
348+name: "ascend950_cluster_32_server_normal"
349+description: "昇腾950 normal组网:32个超级节点,每个超级节点1个服务器"
350+ 
351+# 超节点总数量
352+super_node_num: 4
353+# sever/pod总数量
354+server_num: 32
355+server_list:
356+ # 0-7 server: 均采用ascend950_server_topo_normal拓扑类型
357+ - {super_pod_id: 0, id_range: [0, 7], soc_version: "Ascend950", server_topo: "ascend950_server_topo_normal.yaml"}
358+ - {super_pod_id: 1, id_range: [0, 7], soc_version: "Ascend950", server_topo: "ascend950_server_topo_normal.yaml"}
359+ - {super_pod_id: 2, id_range: [0, 7], soc_version: "Ascend950", server_topo: "ascend950_server_topo_normal.yaml"}
360+ - {super_pod_id: 3, id_range: [0, 7], soc_version: "Ascend950", server_topo: "ascend950_server_topo_normal.yaml"}
361+ 
362+```
363+ 
364+上述配置文件描述了一个包含4个超节点,32个Server,共128个NPU卡的集群拓扑。其中,每个Server/Pod采用ascend950_server_topo_normal拓扑类型。
365+ 
366+**字段说明**
367+ 
368+ - **super_node_num**: 超节点总数量。
369+ - **server_num**: sever/pod总数量。
370+ - **server_list**: 每个Server/Pod的配置信息,包括超节点ID、设备ID范围、芯片型号、Server/Pod拓扑配置文件路径。
371+ 
372+#### 4.3.3 通信域配置文件说明
373+ 
374+用户在昇腾集群环境中,需要根据待执行算子所需的通信域不同,选择不同的通信域配置文件。
375+ 
376+工具提供了hccl-vm mock-comm命令读取和配置算子通信域配置文件。通信域配置文件格式为yaml,路径为hccl_vm_install/cluster_model/topo_meta。若目录中没有对应的通信域配置文件,则用户需要先创建一个。
377+ 
378+hccl-vm工具支持非对称拓扑通信域配置。如下所示:
379+ 
380+```yaml
381+# 1. 全局统计信息 podNum, serNum, rankNum 都小于 1024
382+meta:
383+ podNum: 1 # 总的超节点数
384+ serNum: 2 # 总的server数
385+ rankNum: 6 # 总的rank数
386+ 
387+# 2. 详细拓扑结构
388+topology:
389+ - podId: 0
390+ servers:
391+ - serId: 0
392+ # 每个server实际跑的rank的local id
393+ ranks: [0, 2]
394+ - serId: 1
395+ # 每个server实际跑的rank的local id
396+ ranks: [1, 3, 5, 7]
397+```
398+ 
399+**注意事项**
400+ 
401+- 配置通信域时,工具会根据指定的通信域配置编号重新生成topo.json和ranktable.json文件。
402+- 上述通信域配置yaml文件中,ranks字段表示每个server实际跑的rank的local id(即device的物理ID)列表。
403+ 
404+#### 4.3.4 topo和ranktable.json文件说明
405+ 
406+`topo.json``ranktable.json` 文件不需要手动创建,工具会根据以下信息自动生成:
407+ 
408+- **拓扑配置编号**:用户在启动时指定的编号(如 112、113 等)
409+- **芯片类型**:根据运行环境自动识别的芯片类型
410+ 
411+虽然配置文件由工具自动生成,但了解其结构有助于理解拓扑配置。
412+ 
413+**topo.json 结构**
414+ 
415+`topo.json` 描述了一个 server 内所有设备的连接关系:
416+ 
417+```json
418+{
419+ "server": {
420+ "device_count": 8,
421+ "groups": [
422+ {
423+ "group_id": 0,
424+ "device_start": 0,
425+ "device_count": 8,
426+ "topo_layout": "1D"
427+ }
428+ ]
429+ },
430+ "ports": [
431+ {
432+ "ccu": "die0",
433+ "port_pattern": "0/{0-6}",
434+ "protocol": "HCCS",
435+ "func_id": 2,
436+ "usage": "peer2peer",
437+ "ip_binding": "independent"
438+ },
439+ {
440+ "ccu": "die0",
441+ "port_pattern": "0/7,0/8",
442+ "protocol": "ROCE",
443+ "func_id": 3,
444+ "usage": "peer2net",
445+ "ip_binding": "independent"
446+ }
447+ ],
448+ "links": [
449+ {
450+ "net_layer": 0,
451+ "link_type": "PEER2PEER",
452+ "topo_type": "1DMESH",
453+ "ccu": "die0",
454+ "port_pattern": "0/{0-6}",
455+ "connect_pattern": "full_mesh",
456+ "group_id": 0
457+ },
458+ {
459+ "net_layer": 1,
460+ "link_type": "PEER2NET",
461+ "topo_type": "CLOS",
462+ "ccu": "die0",
463+ "port_pattern": "0/7,0/8",
464+ "connect_pattern": "all_to_net",
465+ "group_id": 0
466+ }
467+ ]
468+}
469+```
470+ 
471+**字段说明**
472+ 
473+- `server.device_count`:设备总数
474+- `server.groups`:设备分组信息
475+- `ports`:端口配置
476+ - `usage`:端口用途(`peer2peer` 表示设备间连接,`peer2net` 表示与外部连接)
477+- `links`:链路配置
478+ - `link_type`:链路类型(`PEER2PEER``PEER2NET`
479+ - `topo_type`:拓扑类型(`1DMESH``CLOS` 等)
480+ 
481+**ranktable.json 结构**
482+ 
483+`ranktable.json` 描述了本次运行使用的设备和 IP 映射:
484+ 
485+```json
486+{
487+ "version": "1.0",
488+ "server_count": 1,
489+ "device_count": 8,
490+ "server_list": [
491+ {
492+ "server_id": 0,
493+ "device_id": 0,
494+ "device_ip": "192.168.1.10",
495+ "port": "2222"
496+ }
497+ ]
498+}
499+```
500+ 
501+**字段说明**
502+ 
503+- `server_count`:服务器数量
504+- `device_count`:设备总数
505+- `server_list`:服务器和设备列表
506+ - `device_ip`:设备 IP 地址
507+ - `port`:设备端口号
508+ 
509+### 4.4 hccl\_config.sh文件说明
510+ 
511+hccl\_config.sh文件中包含了HCCL\_Test用例运行所需的环境变量配置。其中的环境变量与Hccl\_Test用例在真机环境中的环境变量一致。
512+用户需要根据自己的用例和需求,修改hccl\_config.sh脚本,配置HCCL用例运行环境变量。
513+ 
514+```bash
515+#!/bin/bash
516+# hccl_config.sh - HCCL 环境变量配置
517+ 
518+remove_files_by_prefix() {
519+ if [ "$#" -ne 1 ]; then
520+ echo "Usage: remove_files_by_prefix <prefix>" >&2
521+ return 2
522+ fi
523+ 
524+ local prefix="$1"
525+ if [ -z "$prefix" ]; then
526+ return 0
527+ fi
528+ 
529+ shopt -s nullglob
530+ local any_deleted=0
531+ for f in "${prefix}"*; do
532+ if [ -f "$f" ]; then
533+ rm -f -- "$f" && any_deleted=1
534+ fi
535+ done
536+ shopt -u nullglob
537+ 
538+ # 无论是否删除了文件,均返回 0,确保脚本继续执行
539+ return 0
540+}
541+ 
542+# 清理执行目录的冗余文件
543+remove_files_by_prefix "sqe_info_rank_"
544+remove_files_by_prefix "mc_instr_info_rank_"
545+rm -f "all_rank_input_output.txt"
546+ 
547+# 设置CANN环境变量
548+source /home/Ascend/cann/set_env.sh
549+ 
550+# 配置LD_LIBRARY_PATH
551+export LD_LIBRARY_PATH=$ASCEND_HOME_PATH/lib64:$ASCEND_HOME_PATH/devlib:$LD_LIBRARY_PATH
552+ 
553+# 设置 ranktable.json文件路径
554+export RANK_TABLE_FILE=$(pwd)/ranktable.json
555+ 
556+# 设置日志级别
557+export ASCEND_GLOBAL_LOG_LEVEL=1
558+ 
559+# 设置日志打屏输出
560+export ASCEND_SLOG_PRINT_TO_STDOUT=1
561+ 
562+# 设置 HCCL 运行模式(CCU、AI_CPU、AIV等展开模式)
563+export HCCL_OP_EXPANSION_MODE="CCU_SCHED"
564+# 或设置 HCCL 运行参数(AI_CPU展开模式) AI_CPU模式环境变量与其他模式不可同时设置
565+# export HCCL_OP_EXPANSION_MODE="AI_CPU"
566+# 或设置 HCCL 运行参数(AIV展开模式) AIV模式环境变量与其他模式不可同时设置
567+# export HCCL_OP_EXPANSION_MODE="AIV"
568+ 
569+echo "HCCL-VM environment configured successfully!"
570+ 
571+```
572+ 
573+### 4.5 工具规格约束
574+ 
575+**支持算子类型**
576+ 
577+工具支持算子类型包含:allgather/allreduce/alltoall/reduce/reduce\_scatter/scatter/alltoallv.
578+ 
579+**支持数据类型**
580+ 
581+HCCL-VM工具支持数据类型包含:int8/int16/int32/fp16/fp32/uint8/uint16/uint32/bfp16/hif8/fp8e4m3/fp8e5m2/fp8e8m0.
582+ 
583+HCCL-VM Runner插件支持数据类如下:
584+ 
585+| ReduceOp | DataType |
586+| -------- | ---------------------------------- |
587+| `ADD` | `int8/int16/int32/uint8` |
588+| `MIN` | `int8/int16/int32/uint8` |
589+| `MAX` | `int8/int16/int32/uint8` |
590+ 
591+**硬件规格**
592+ 
593+目前本工具仅适配支持Ascend950芯片。单server内最大支持8张卡;超过8张卡,需要跨server运行。
594+ 
595+### 4.6 HCCL-VM插件功能
596+ 
597+#### 4.6.1 Runner插件
598+ 
599+Runner插件,即模拟执行Hccl业务编排生成的任务,输出output数据。
600+模拟运行器插件在hccl\_test用例执行过程中默认**关闭**。hccl\_test用例调用算子接口后,通过aclrtSynchronizeStream接口等待算子任务执行完成。模拟运行器工具会等待所有rank都处于等待状态时,开启模拟执行所有rank的task。执行完成后,通知各个rank的用例进行继续执行。
601+用例执行完成后,用户可以通过执行目录下的"all\_rank\_input\_output.txt"文件,查看每个rank的input buffer和output buffer数据。此功能默认不开启,用户可以在用例执行前通过对应命令开启。
602+ 
603+**安装与卸载**
604+ 
605+Runner插件支持通过 `hccl-vm plugin install/uninstall` 命令进行安装和卸载。需在进入hccl-vm工具命令行后、执行算例前安装runner插件,后续的执行才会运行runner。
606+ 
607+```bash
608+# 安装runner插件
609+(hvm)$> hccl-vm plugin install @runner
610+ 
611+# 卸载runner插件
612+(hvm)$> hccl-vm plugin uninstall @runner
613+```
614+ 
615+#### 4.6.2 Checker插件
616+ 
617+Checker插件,即算法分析器插件:功能是将hccl生成的所有task形成一个DAG图,并且通过分析DAG图,判断是否存在内存冲突;通过模拟执行DAG图,判断是否存在语义错误等问题。
618+算法分析器插件,是由用户自行通过命令启动执行。
619+ 
620+Checker插件正处于新旧交替阶段,Checker V3为原Checker的重构版,主要提高了校验性能,在默认情况下将会运行新Checker(Checker V3),可以通过修改Checker的`manifest.json`文件中的配置参数进行调整。
621+ 
622+```bash
623+# 配置文件位于 /pathto/hccl_vm_install/plugin/validate/checker/manifest.json
624+ 
625+{
626+ "name": "checker", // Checker插件名
627+ "version": "1.0.0", // Checker插件版本
628+ "entry": "./checker", // Checker插件启动指令
629+ "dependency": {
630+ "min_core_version": "1.0.0"
631+ },
632+ "setting": { // Checker插件配置项
633+ "enable_new_checker": true, // 是否启用新Checker(Checker V3,默认开启)
634+ "enable_old_checker": false, // 是否启用老Checker(默认关闭)
635+ "enable_insight_dump": false, // 是否启用可视化数据输出(默认关闭,仅支持老Checker)
636+ "enable_memory_snapshot_dump": false // 是否启用可视化内存快照数据输出(默认关闭,仅支持老Checker,需要先开启可视化数据输出"enable_insight_dump")
637+ }
638+}
639+```
640+ 
641+### 4.7 hccl_rootinfo.json文件
642+ 
643+目前工具初始化通信域使用的是ranktable.json文件,因此hccl_rootinfo.json文件的作用仅限于提供topo.json文件的路径。
644+若/etc路径下没有hccl_rootinfo.json文件,则用户需自行创建该文件,内容如下:
645+ 
646+```json
647+{
648+ "version": "2.0",
649+ "topo_file_path": "/home/myuser/workspace/CheckerL2/hccl_vm_install/topo.json"
650+}
651+```
652+ 
653+### 4.8 OpenMPI和MPICH环境用例运行差异
654+ 
655+运行hccl_test用例前,用户可以通过which命令判断当前环境使用的是哪个mpirun。
656+ 
657+#### 4.8.1 环境变量配置差异
658+ 
659+环境中一般默认配置的是OpenMPI,若用户使用OpenMPI运行用例,一般不需要额外配置环境变量。
660+若用户使用MPICH环境运行用例,需要按照如下方式配置环境变量:
661+ 
662+```bash
663+# 配置mpich环境变量
664+export LD_LIBRARY_PATH=/usr/lib/mpich/lib/:${ASCEND_HOME_PATH}/lib64/:${ASCEND_HOME_PATH}/x86_64-linux/devlib:$LD_LIBRARY_PATH
665+export PATH=/usr/lib/mpich/bin:$PATH
666+```
667+ 
668+#### 4.8.2 mpirun命令参数差异
669+ 
670+OpenMPI环境,用户按照如下命令运行hccl_test用例:
671+ 
672+```bash
673+export HCCL_TEST_PATH=/home/myuser/workspace/Ascend/cann/tools/hccl_test
674+mpirun --allow-run-as-root --oversubscribe -np 2 ${HCCL_TEST_PATH}/bin/reduce_scatter_test -b 64 -e 64 -d int32 -o sum -w 0 -n 1 -c 1
675+```
676+ 
677+**参数说明**
678+ 
679+ - --allow-run-as-root:OpenMPI专属参数,允许以root用户运行MPI进程,用于在没有root权限的环境中运行。
680+ - --oversubscribe:OpenMPI专属参数,放开CPU槽位(slot)限制,允许单节点启动[进程数>CPU逻辑核数],也就是超额超配跑进程。
681+ - -np 2:指定进程数为2,与节点数一致。
682+ 
683+MPICH环境,用户按照如下命令运行hccl_test用例:
684+ 
685+```bash
686+export HCCL_TEST_PATH=/home/myuser/workspace/Ascend/cann/tools/hccl_test
687+mpirun -np 2 ${HCCL_TEST_PATH}/bin/reduce_scatter_test -b 64 -e 64 -d int32 -o sum -w 0 -n 1 -c 1
688+```
689+ 
690+**参数说明**
691+ 
692+ - -np 2:指定进程数为2,与节点数一致。
693+ 
694+### 4.9 结果查看
695+ 
696+#### 4.9.1 Runner插件结果
697+ 
698+如果在hccl-vm终端执行hccl-vm plugin install @runner安装插件后,算子流程执行完成后会自动触发runner插件的执行,最终结果依赖hccl_test进行校验,用户需关注在重定向的日志文件中是否存在[error]级别日志和最终校验结果:
699+ 
700+```bash
701+data_size(Bytes): | aveg_time(us): | alg_bandwidth(GB/s): | check_result:
702+64 | 1000.00 | 0.00006 | success
703+```
704+ 
705+#### 4.9.2 Checker插件结果
706+ 
707+在hccl-vm终端内执行hccl-vm plugin run @checker后,Checker校验流程及结果会打印在终端内,用于需关注是否存在[error]级别日志和最终校验结果:
708+ 
709+```bash
710+[info][PID:144373][TID:144880][main.cc][RunChecker] [RunChecker] op[0] Checker Success.
711+```
712+ 
713+---
714+ 
715+## 5 附录
716+ 
717+### 开源第三方软件依赖
718+ 
719+编译本项目时,依赖的第三方开源软件列表如下,离线编译场景可下载并重命名软件包后放置在本项目内的third_party目录下
720+ 
721+| 开源软件 | 版本 |下载地址 |
722+| ------------ | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
723+| CLI11 | 2.2.0 | [cli11-2.2.0.tar.gz](https://raw.gitcode.com/src-openeuler/cli11/blobs/58c912141164a5c0f0139bfa91343fefe151d525/cli11-2.2.0.tar.gz) |
724+| json | 3.11.3 | [include.zip](https://gitcode.com/cann-src-third-party/json/releases/download/v3.11.3/include.zip) |
725+| spdlog | 1.11.0 | [spdlog-v1.11.0.tar.gz](https://raw.gitcode.com/src-openeuler/spdlog/blobs/c2dfb1aca26c607393665c836155613ff283de66/v1.11.0.tar.gz) |
726+| yaml-cpp | 0.8.0 | [yaml-cpp-0.8.0.tar.gz](https://raw.gitcode.com/src-openeuler/yaml-cpp/blobs/d1ead4fff417073b9cdbf98b8b55eb0efc00b0ba/yaml-cpp-0.8.0.tar.gz) |
727+| sqlite | 3.51.0 | [sqlite-amalgamation-3510300.zip](https://www.sqlite.org/2026/sqlite-amalgamation-3510300.zip) |
728+| googletest | 1.14.0 | [googletest-1.14.0.tar.gz](https://gitcode.com/cann-src-third-party/googletest/releases/download/v1.14.0/googletest-1.14.0.tar.gz) |
729+ 
730+### 术语表
731+ 
732+| 术语 | 说明 |
733+| -------- | ------------------------------------------------------- |
734+| HCCL | Huawei Collective Communication Library,华为集合通信库 |
735+| NPU | Neural Processing Unit,神经网络处理器 |
736+| CANN | Compute Architecture for Neural Networks,华为昇腾 AI 处理器软件栈 |
737+| MPI | Message Passing Interface,消息传递接口 |
738+| CCU | Collective Communication Unit,集合通信单元 |
739+| Topology | 拓扑,设备连接关系 |
740+| Rank | 进程编号,在 MPI 中的标识 |
741+ 
742+---
743+ 
744+**文档版本**:v1.0
745+**最后更新**:2026-06-22
@@ -0,0 +1,252 @@
1+#!/bin/bash
2+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
4+# CANN Open Software License Agreement Version 2.0 (the "License").
5+# Please refer to the License for details. You may not use this file except in compliance with the License.
6+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
7+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
8+# See LICENSE in the root of the software repository for the full text of the License.
9+ 
10+# 统一分配EID/IP地址
11+# 功能:
12+# 1. 遍历output_temp目录下所有rootinfo相关json文件
13+# 2. 根据IP地址分配规则,为每个server的portGroup分配IP地址
14+# 3. 将IP地址转换为EID字符串,修改rootinfo文件中的addr字段
15+#
16+# IP地址分配规则 (AA.BB.CC.DD):
17+# AA = 192 + superpod_index (超节点段,从192开始)
18+# BB = server_index + 1 (服务器段,从1开始,排除192.0.0.0~192.0.0.24段)
19+# CC:DD = device port计数器 (设备段,从0.1开始)
20+# Host IP: AA.BB.0.0 (每个server固定一个host IP,仅供host使用)
21+# Device port IPs: AA.BB.0.1, AA.BB.0.2, ... (依次递增,d2h端口也从此段分配)
22+#
23+# IP→EID转换规则 (等价于C++ IpToEidString):
24+# IPv4地址(4字节)放入16字节EID的末尾(网络字节序),前12字节为0
25+# 输出格式: "0x" + 32位十六进制字符串
26+# 示例: 192.0.0.1 → 0x000000000000000000000000c0000001
27+ 
28+set -e
29+ 
30+RED='\033[0;31m'
31+GREEN='\033[0;32m'
32+YELLOW='\033[1;33m'
33+BLUE='\033[0;34m'
34+NC='\033[0m'
35+ 
36+error_exit() {
37+ echo -e "${RED}[错误] $1${NC}" >&2
38+ exit 1
39+}
40+ 
41+success_msg() {
42+ echo -e "${GREEN}[成功] $1${NC}"
43+}
44+ 
45+info_msg() {
46+ echo -e "${BLUE}[信息] $1${NC}"
47+}
48+ 
49+warn_msg() {
50+ echo -e "${YELLOW}[警告] $1${NC}"
51+}
52+ 
53+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
54+BASE_DIR="${SCRIPT_DIR}"
55+OUTPUT_DIR="${BASE_DIR}/cluster_model/network/cluster"
56+ 
57+show_usage() {
58+ cat << EOF
59+用法: $(basename "$0") [OPTIONS]
60+ 
61+选项:
62+ -d, --dir DIR 指定集群输出目录 (默认: ${OUTPUT_DIR})
63+ -h, --help 显示帮助信息
64+ 
65+说明:
66+ 遍历output_temp目录下所有rootinfo JSON文件,按照IP地址分配规则
67+ 统一分配IP地址,转换为EID字符串后写入addr字段。
68+ 
69+ IP地址分配规则 (AA.BB.CC.DD):
70+ AA = 192 + superpod_index (超节点段)
71+ BB = server_index + 1 (服务器段,从1开始,排除192.0.0.0~192.0.0.24段)
72+ CC:DD = device port计数器 (设备段,从0.1开始)
73+ Host IP: AA.BB.0.0 (每个server固定一个host IP,仅供host使用)
74+ Device port IPs: AA.BB.0.1, AA.BB.0.2, ... (依次递增,d2h端口也从此段分配)
75+ 
76+ IP→EID转换:
77+ IPv4地址放入16字节EID末尾(网络字节序),前12字节为0
78+ 示例: 192.0.0.1 → 0x000000000000000000000000c0000001
79+ 
80+示例:
81+ $(basename "$0")
82+ $(basename "$0") -d /tmp/my_output
83+ 
84+EOF
85+}
86+ 
87+while [[ $# -gt 0 ]]; do
88+ case $1 in
89+ -h|--help)
90+ show_usage
91+ exit 0
92+ ;;
93+ -d|--dir)
94+ OUTPUT_DIR="$2"
95+ shift 2
96+ ;;
97+ *)
98+ error_exit "未知参数: $1\n$(show_usage)"
99+ ;;
100+ esac
101+done
102+ 
103+if [[ ! -d "$OUTPUT_DIR" ]]; then
104+ error_exit "输出目录不存在: $OUTPUT_DIR\n请先运行 generate_cluster_topo.sh 生成集群拓扑目录"
105+fi
106+ 
107+echo "=========================================="
108+echo " 昇腾集群EID统一分配工具"
109+echo "=========================================="
110+echo ""
111+echo "输出目录: $OUTPUT_DIR"
112+echo ""
113+ 
114+export OUTPUT_DIR
115+ 
116+python3 << 'PYTHON_SCRIPT'
117+import json
118+import os
119+import re
120+import sys
121+import struct
122+import socket
123+ 
124+OUTPUT_DIR = os.environ.get('OUTPUT_DIR', '')
125+if not OUTPUT_DIR:
126+ print("[错误] OUTPUT_DIR 环境变量未设置")
127+ sys.exit(1)
128+ 
129+URMA_EID_LEN = 16
130+ 
131+def ip_to_eid_string(ip, family=socket.AF_INET):
132+ """
133+ 将IP地址转换为EID字符串。
134+ 等价于C++函数:
135+ std::string IpToEidString(const std::string& ip, int family = AF_INET)
136+ 
137+ 转换逻辑:
138+ 1. 将IPv4地址解析为4字节(网络字节序)
139+ 2. 构造16字节EID: 前12字节为0,后4字节为IPv4地址
140+ 3. 输出 "0x" + 32位十六进制字符串
141+ 
142+ 示例:
143+ ip_to_eid_string("192.0.0.1") -> "0x000000000000000000000000c0000001"
144+ ip_to_eid_string("192.0.0.16") -> "0x000000000000000000000000c0000010"
145+ ip_to_eid_string("193.3.0.5") -> "0x000000000000000000000000c1030005"
146+ """
147+ if family == socket.AF_INET:
148+ ip_bytes = socket.inet_pton(socket.AF_INET, ip)
149+ eid = bytearray(URMA_EID_LEN)
150+ eid[12:16] = ip_bytes
151+ elif family == socket.AF_INET6:
152+ ip_bytes = socket.inet_pton(socket.AF_INET6, ip)
153+ eid = bytearray(URMA_EID_LEN)
154+ eid[0:16] = ip_bytes
155+ else:
156+ raise ValueError(f"不支持的地址族: {family}")
157+ 
158+ hex_str = ''.join(f'{b:02x}' for b in eid)
159+ return f'0x{hex_str}'
160+ 
161+def find_rootinfo_files(output_dir):
162+ rootinfo_files = []
163+ for root, dirs, files in os.walk(output_dir):
164+ for f in files:
165+ if 'rootinfo' in f and f.endswith('.json'):
166+ rootinfo_files.append(os.path.join(root, f))
167+ return rootinfo_files
168+ 
169+def extract_indices(filepath):
170+ match = re.search(r'superpod(\d+)/server(\d+)', filepath)
171+ if match:
172+ return (int(match.group(1)), int(match.group(2)))
173+ return (-1, -1)
174+ 
175+def allocate_ip(aa, bb, counter):
176+ cc = (counter >> 8) & 0xFF
177+ dd = counter & 0xFF
178+ return f"{aa}.{bb}.{cc}.{dd}"
179+ 
180+rootinfo_files = find_rootinfo_files(OUTPUT_DIR)
181+ 
182+if not rootinfo_files:
183+ print("[警告] 未找到任何rootinfo JSON文件")
184+ sys.exit(0)
185+ 
186+rootinfo_files.sort(key=extract_indices)
187+ 
188+print(f"[信息] 找到 {len(rootinfo_files)} 个rootinfo文件")
189+print(f"[信息] IP→EID转换规则: IPv4地址放入16字节EID末尾(网络字节序)")
190+print("")
191+ 
192+total_addrs = 0
193+ 
194+for filepath in rootinfo_files:
195+ sp_idx, srv_idx = extract_indices(filepath)
196+ 
197+ if sp_idx < 0 or srv_idx < 0:
198+ print(f" [警告] 无法从路径提取索引,跳过: {filepath}")
199+ continue
200+ 
201+ aa = 192 + sp_idx
202+ bb = srv_idx + 1
203+ 
204+ host_ip = f"{aa}.{bb}.0.0"
205+ host_eid = ip_to_eid_string(host_ip)
206+ 
207+ with open(filepath, 'r') as f:
208+ data = json.load(f)
209+ 
210+ DEVICE_PORT_START = 1
211+ port_counter = DEVICE_PORT_START
212+ addr_count = 0
213+ first_device_eid = None
214+ last_device_eid = None
215+ 
216+ for rank in data.get('rank_list', []):
217+ for level in rank.get('level_list', []):
218+ for addr_entry in level.get('rank_addr_list', []):
219+ ip_addr = allocate_ip(aa, bb, port_counter)
220+ eid_str = ip_to_eid_string(ip_addr)
221+ addr_entry['addr'] = eid_str
222+ if first_device_eid is None:
223+ first_device_eid = eid_str
224+ last_device_eid = eid_str
225+ port_counter += 1
226+ addr_count += 1
227+ 
228+ with open(filepath, 'w') as f:
229+ json.dump(data, f, indent=4, ensure_ascii=False)
230+ 
231+ print(f" [成功] superpod{sp_idx}/server{srv_idx}: "
232+ f"分配 {addr_count} 个EID")
233+ print(f" Host IP: {host_ip} → EID: {host_eid}")
234+ if first_device_eid:
235+ last_cc = ((port_counter - 1) >> 8) & 0xFF
236+ last_dd = (port_counter - 1) & 0xFF
237+ print(f" Device IP: {aa}.{bb}.0.1 ~ {aa}.{bb}.{last_cc}.{last_dd}")
238+ print(f" Device EID: {first_device_eid} ~ {last_device_eid}")
239+ 
240+ total_addrs += addr_count
241+ 
242+print("")
243+print(f"[信息] 共处理 {len(rootinfo_files)} 个文件,分配 {total_addrs} 个EID")
244+ 
245+PYTHON_SCRIPT
246+ 
247+echo ""
248+echo "=========================================="
249+echo " 分配完成"
250+echo "=========================================="
251+echo ""
252+success_msg "EID统一分配完成!"
@@ -0,0 +1,13 @@
1+name: "ascend950_cluster_32_server_normal"
2+description: "昇腾950 normal组网:32个超级节点,每个超级节点1个服务器"
3+ 
4+# 超节点总数量
5+super_node_num: 4
6+# sever/pod总数量
7+server_num: 128
8+server_list:
9+ # 0-31 server: 均采用ascend950_server_topo_normal拓扑类型
10+ - {super_pod_id: 0, id_range: [0, 31], soc_version: "Ascend950", server_topo: "ascend950_server_topo_normal.yaml"}
11+ - {super_pod_id: 1, id_range: [0, 31], soc_version: "Ascend950", server_topo: "ascend950_server_topo_normal.yaml"}
12+ - {super_pod_id: 2, id_range: [0, 31], soc_version: "Ascend950", server_topo: "ascend950_server_topo_normal.yaml"}
13+ - {super_pod_id: 3, id_range: [0, 31], soc_version: "Ascend950", server_topo: "ascend950_server_topo_normal.yaml"}
@@ -0,0 +1,11 @@
1+name: "ascend950_cluster_32_server_normal"
2+description: "昇腾950 normal组网:32个超级节点,每个超级节点1个服务器"
3+ 
4+# 超节点总数量
5+super_node_num: 2
6+# sever/pod总数量
7+server_num: 16
8+server_list:
9+ # 0-7 server: 均采用ascend950_server_topo_normal拓扑类型
10+ - {super_pod_id: 0, id_range: [0, 7], soc_version: "Ascend950", server_topo: "ascend950_server_topo_normal.yaml"}
11+ - {super_pod_id: 1, id_range: [0, 7], soc_version: "Ascend950", server_topo: "ascend950_server_topo_normal.yaml"}
@@ -0,0 +1,13 @@
1+name: "ascend950_cluster_32_server_normal"
2+description: "昇腾950 normal组网:32个超级节点,每个超级节点1个服务器"
3+ 
4+# 超节点总数量
5+super_node_num: 4
6+# sever/pod总数量
7+server_num: 32
8+server_list:
9+ # 0-7 server: 均采用ascend950_server_topo_normal拓扑类型
10+ - {super_pod_id: 0, id_range: [0, 7], soc_version: "Ascend950", server_topo: "ascend950_server_topo_normal.yaml"}
11+ - {super_pod_id: 1, id_range: [0, 7], soc_version: "Ascend950", server_topo: "ascend950_server_topo_normal.yaml"}
12+ - {super_pod_id: 2, id_range: [0, 7], soc_version: "Ascend950", server_topo: "ascend950_server_topo_normal.yaml"}
13+ - {super_pod_id: 3, id_range: [0, 7], soc_version: "Ascend950", server_topo: "ascend950_server_topo_normal.yaml"}
@@ -0,0 +1,11 @@
1+name: "ascend950_cluster_4_server_1dpod64"
2+description: "昇腾950 1DPod64集群组网:4个超级节点,4个服务器每个超级节点1个服务器"
3+ 
4+# 超节点总数量
5+super_node_num: 2
6+# sever/pod总数量
7+server_num: 4
8+server_list:
9+ # 0-1 server: 均采用ascend950_server_topo_1dpod64拓扑类型
10+ - {super_pod_id: 0, id_range: [0, 1], soc_version: "Ascend950", server_topo: "ascend950_server_topo_1dpod64.yaml"}
11+ - {super_pod_id: 1, id_range: [0, 1], soc_version: "Ascend950", server_topo: "ascend950_server_topo_1dpod64.yaml"}
@@ -0,0 +1,10 @@
1+name: "ascend950_cluster_4_server_competition"
2+description: "昇腾950 competition组网:32个超级节点,每个超级节点1个服务器"
3+ 
4+# 超节点总数量
5+super_node_num: 1
6+# sever/pod总数量
7+server_num: 4
8+server_list:
9+ # 0-3 server: 均采用ascend950_server_topo_competition拓扑类型
10+ - {super_pod_id: 0, id_range: [0, 3], soc_version: "Ascend950", server_topo: "ascend950_server_topo_competition.yaml"}
@@ -0,0 +1,11 @@
1+name: "ascend950_cluster_32_server_normal"
2+description: "昇腾950 normal组网:32个超级节点,每个超级节点1个服务器"
3+ 
4+# 超节点总数量
5+super_node_num: 2
6+# sever/pod总数量
7+server_num: 4
8+server_list:
9+ # 0-1 server: 均采用ascend950_server_topo_normal拓扑类型
10+ - {super_pod_id: 0, id_range: [0, 1], soc_version: "Ascend950", server_topo: "ascend950_server_topo_normal.yaml"}
11+ - {super_pod_id: 1, id_range: [0, 1], soc_version: "Ascend950", server_topo: "ascend950_server_topo_normal.yaml"}
@@ -0,0 +1,78 @@
1+type: "server_intra_links"
2+name: "ascend950_links_topo_1dpod64"
3+description: "ascend950芯片1dpod64类型拓扑连接关系描述文件"
4+ 
5+soc_version: "Ascend950"
6+device_num: 64
7+ 
8+device_group:
9+ - {group_id: 1, device_list: [0, 1, 2, 3, 8, 9, 10, 11, 16, 17, 18, 19, 24, 25, 26, 27, 32, 33, 34, 35, 40, 41, 42, 43, 48, 49, 50, 51, 56, 57, 58, 59]}
10+ - {group_id: 2, device_list: [4, 5, 6, 7, 12, 13, 14, 15, 20, 21, 22, 23, 28, 29, 30, 31, 36, 37, 38, 39, 44, 45, 46, 47, 52, 53, 54, 55, 60, 61, 62, 63]}
11+ 
12+device_ports_allocate_map:
13+ # port分配表: 0: 不使用, 1: device直连, 2: device连交换机, 3: d2h端口
14+ # portId: 0 1 2 3 4 5 6 7 8
15+ # 端口分配表1: 适用于device group 1
16+ - group_id: 1
17+ - {die_id: 0, pin_map: [1, 2, 2, 1, 1, 1, 1, 1 , 1]} # die0
18+ - {die_id: 1, pin_map: [2, 2, 2, 2, 0, 2, 2, 0, 3]} # die1
19+ # 端口分配表2: 适用于device group 2
20+ - group_id: 2
21+ # portId: 0 1 2 3 4 5 6 7 8
22+ - {die_id: 0, pin_map: [2, 2, 2, 2, 2, 2, 0, 0, 3]} # die0
23+ - {die_id: 1, pin_map: [1, 2, 2, 1, 1, 1, 1, 1, 1]} # die1
24+ 
25+# port_list: topo.json文件中各netlayer的port列表
26+port_list:
27+ - group_id: 1
28+ - {layer: 0, ports: ["0/1", "0/2", "1/0", "1/1", "1/2", "1/3", "1/5", "1/6"]}
29+ - {layer: 1, ports: ["0/1", "0/2", "1/0", "1/1", "1/2", "1/3", "1/5", "1/6"]}
30+ - {layer: 2, ports: ["0/1", "0/2", "1/1", "1/2"]}
31+ - {layer: 3, ports: ["d2h"]}
32+ - group_id: 2
33+ - {layer: 0, ports: ["1/1", "1/2", "0/0", "0/1", "0/2", "0/3", "0/4", "0/5"]}
34+ - {layer: 1, ports: ["1/1", "1/2", "0/0", "0/1", "0/2", "0/3", "0/4", "0/5"]}
35+ - {layer: 2, ports: ["0/1", "0/2", "1/1", "1/2"]}
36+ - {layer: 3, ports: ["d2h"]}
37+ 
38+ 
39+# port_group: rootinfo.json文件中的portGroup,相同portGroup的port对于IP地址相同
40+port_group:
41+ - group_id: 1
42+ - {layer: 1, ports: ["0/1", "0/2"]}
43+ - {layer: 1, ports: ["1/0", "1/1", "1/2", "1/3", "1/5", "1/6"]}
44+ - group_id: 2
45+ - {layer: 1, ports: ["1/1", "1/2"]}
46+ - {layer: 1, ports: ["0/0", "0/1", "0/2", "0/3", "0/4", "0/5"]}
47+ 
48+links:
49+ # ── 描述法:每行8个device组成全互联 ──
50+ - link_mode: "fullmesh"
51+ connections:
52+ - {die_id: 0, devices: [0, 1, 2, 3]}
53+ - {die_id: 1, devices: [4, 5, 6, 7]}
54+ - {die_id: 0, devices: [8, 9, 10, 11]}
55+ - {die_id: 1, devices: [12, 13, 14, 15]}
56+ - {die_id: 0, devices: [16, 17, 18, 19]}
57+ - {die_id: 1, devices: [20, 21, 22, 23]}
58+ - {die_id: 0, devices: [24, 25, 26, 27]}
59+ - {die_id: 1, devices: [28, 29, 30, 31]}
60+ - {die_id: 0, devices: [32, 33, 34, 35]}
61+ - {die_id: 1, devices: [36, 37, 38, 39]}
62+ - {die_id: 0, devices: [40, 41, 42, 43]}
63+ - {die_id: 1, devices: [44, 45, 46, 47]}
64+ - {die_id: 0, devices: [48, 49, 50, 51]}
65+ - {die_id: 1, devices: [52, 53, 54, 55]}
66+ - {die_id: 0, devices: [56, 57, 58, 59]}
67+ - {die_id: 1, devices: [60, 61, 62, 63]}
68+ 
69+ - link_mode: "enum"
70+ device_to_device_links:
71+ - {src_die_id: 0, src_local_id: [0, 1, 2, 3], dst_die_id: 1, dst_local_id: [4, 5, 6, 7]}
72+ - {src_die_id: 0, src_local_id: [8, 9, 10, 11], dst_die_id: 1, dst_local_id: [12, 13, 14, 15]}
73+ - {src_die_id: 0, src_local_id: [16, 17, 18, 19], dst_die_id: 1, dst_local_id: [20, 21, 22, 23]}
74+ - {src_die_id: 0, src_local_id: [24, 25, 26, 27], dst_die_id: 1, dst_local_id: [28, 29, 30, 31]}
75+ - {src_die_id: 0, src_local_id: [32, 33, 34, 35], dst_die_id: 1, dst_local_id: [36, 37, 38, 39]}
76+ - {src_die_id: 0, src_local_id: [40, 41, 42, 43], dst_die_id: 1, dst_local_id: [44, 45, 46, 47]}
77+ - {src_die_id: 0, src_local_id: [48, 49, 50, 51], dst_die_id: 1, dst_local_id: [52, 53, 54, 55]}
78+ - {src_die_id: 0, src_local_id: [56, 57, 58, 59], dst_die_id: 1, dst_local_id: [60, 61, 62, 63]}
@@ -0,0 +1,32 @@
1+type: "server_intra_links"
2+name: "ascend950_links_topo_competition"
3+description: "ascend950芯片competition类型拓扑连接关系描述文件"
4+ 
5+soc_version: "Ascend950"
6+device_num: 8
7+ 
8+device_ports_allocate_map:
9+ # port分配表: 0: 不使用, 1: device直连, 2: device连交换机, 3: d2h端口
10+ # portId: 0 1 2 3 4 5 6 7 8
11+ - {die_id: 0, pin_map: [0, 1, 1, 1, 2, 2, 2, 2, 0]} # die0
12+ - {die_id: 1, pin_map: [1, 1, 1, 1, 0, 2, 2, 0, 0]} # die1
13+ 
14+port_group:
15+ - {layer: 1, ports: ["0/4", "0/5", "0/6", "0/7", "1/5", "1/6"]}
16+ 
17+links:
18+ - link_mode: "enum"
19+ device_to_device_links:
20+ - {src_die_id: 1, src_local_id_range: [0], dst_die_id: 1, dst_local_id_range: [1, 3, 5, 7]}
21+ - {src_die_id: 0, src_local_id_range: [0], dst_die_id: 1, dst_local_id_range: [2, 4, 6]}
22+ - {src_die_id: 0, src_local_id_range: [1], dst_die_id: 0, dst_local_id_range: [2, 4, 6]}
23+ - {src_die_id: 1, src_local_id_range: [1], dst_die_id: 0, dst_local_id_range: [3, 5, 7]}
24+ - {src_die_id: 1, src_local_id_range: [2], dst_die_id: 1, dst_local_id_range: [3, 5, 7]}
25+ - {src_die_id: 0, src_local_id_range: [2], dst_die_id: 1, dst_local_id_range: [4, 6]}
26+ - {src_die_id: 0, src_local_id_range: [3], dst_die_id: 0, dst_local_id_range: [4, 6]}
27+ - {src_die_id: 1, src_local_id_range: [3], dst_die_id: 0, dst_local_id_range: [5, 7]}
28+ - {src_die_id: 1, src_local_id_range: [4], dst_die_id: 1, dst_local_id_range: [5, 7]}
29+ - {src_die_id: 0, src_local_id_range: [4], dst_die_id: 1, dst_local_id_range: [6]}
30+ - {src_die_id: 0, src_local_id_range: [5], dst_die_id: 0, dst_local_id_range: [6]}
31+ - {src_die_id: 1, src_local_id_range: [5], dst_die_id: 0, dst_local_id_range: [7]}
32+ - {src_die_id: 1, src_local_id_range: [6], dst_die_id: 1, dst_local_id_range: [7]}
@@ -0,0 +1,29 @@
1+type: "server_intra_links"
2+name: "ascend950_links_topo_normal"
3+description: "ascend950芯片NORMAL类型拓扑连接关系描述文件"
4+ 
5+soc_version: "Ascend950"
6+device_num: 8
7+hardware_type: "910D-2D-Fullmsh_64_plus_1"
8+ 
9+device_ports_allocate_map:
10+ # port分配表: 0: 不使用, 1: device直连, 2: device连交换机, 3: d2h端口
11+ # portId: 0 1 2 3 4 5 6 7 8
12+ - {die_id: 0, pin_map: [2, 2, 2, 2, 2, 2, 2, 2, 3]} # die0
13+ - {die_id: 1, pin_map: [1, 1, 1, 1, 1, 1, 1, 0, 0]} # die1
14+ 
15+# port_group: 描述哪些port合并为一个portGroup,相同portGroup的port对于IP地址相同
16+port_group:
17+ - {layer: 1, ports: ["0/0", "0/1", "0/2", "0/3", "0/4", "0/5", "0/6", "0/7"]}
18+ - {layer: 2, ports: ["0/7"]}
19+ - {layer: 3, ports: ["d2h"]}
20+ 
21+links:
22+ # ── 描述法:每行8个device组成全互联 ──
23+ - link_mode: "fullmesh"
24+ connections:
25+ - {die_id: 1, devices_range: [0, 7]}
26+ 
27+ - link_mode: "enum"
28+ device_to_switch_links:
29+ - {die_id: 0, devices_range: [0, 7]}
@@ -0,0 +1,12 @@
1+# 1. 全局统计信息 podNum, serNum, rankNum 都小于 1024
2+meta:
3+ podNum: 1
4+ serNum: 1
5+ rankNum: 2
6+ 
7+# 2. 详细拓扑结构
8+topology:
9+ - podId: 0
10+ servers:
11+ - serId: 0
12+ ranks: [0, 1]
@@ -0,0 +1,12 @@
1+# 1. 全局统计信息 podNum, serNum, rankNum 都小于 1024
2+meta:
3+ podNum: 1
4+ serNum: 1
5+ rankNum: 4
6+ 
7+# 2. 详细拓扑结构
8+topology:
9+ - podId: 0
10+ servers:
11+ - serId: 0
12+ ranks: [0, 1, 2, 3]
@@ -0,0 +1,12 @@
1+# 1. 全局统计信息 podNum, serNum, rankNum 都小于 1024
2+meta:
3+ podNum: 1
4+ serNum: 1
5+ rankNum: 8
6+ 
7+# 2. 详细拓扑结构
8+topology:
9+ - podId: 0
10+ servers:
11+ - serId: 0
12+ ranks: [0, 1, 2, 3, 4, 5, 6, 7]
@@ -0,0 +1,14 @@
1+# 1. 全局统计信息 podNum, serNum, rankNum 都小于 1024
2+meta:
3+ podNum: 1
4+ serNum: 2
5+ rankNum: 2
6+ 
7+# 2. 详细拓扑结构
8+topology:
9+ - podId: 0
10+ servers:
11+ - serId: 0
12+ ranks: [0]
13+ - serId: 1
14+ ranks: [0]
@@ -0,0 +1,14 @@
1+# 1. 全局统计信息 podNum, serNum, rankNum 都小于 1024
2+meta:
3+ podNum: 1
4+ serNum: 2
5+ rankNum: 4
6+ 
7+# 2. 详细拓扑结构
8+topology:
9+ - podId: 0
10+ servers:
11+ - serId: 0
12+ ranks: [0, 1]
13+ - serId: 1
14+ ranks: [0, 1]
@@ -0,0 +1,14 @@
1+# 1. 全局统计信息 podNum, serNum, rankNum 都小于 1024
2+meta:
3+ podNum: 1
4+ serNum: 2
5+ rankNum: 8
6+ 
7+# 2. 详细拓扑结构
8+topology:
9+ - podId: 0
10+ servers:
11+ - serId: 0
12+ ranks: [0, 1, 2, 3]
13+ - serId: 1
14+ ranks: [0, 1, 2, 3]
@@ -0,0 +1,14 @@
1+# 1. 全局统计信息 podNum, serNum, rankNum 都小于 1024
2+meta:
3+ podNum: 1
4+ serNum: 2
5+ rankNum: 16
6+ 
7+# 2. 详细拓扑结构
8+topology:
9+ - podId: 0
10+ servers:
11+ - serId: 0
12+ ranks: [0, 1, 2, 3, 4, 5, 6, 7]
13+ - serId: 1
14+ ranks: [0, 1, 2, 3, 4, 5, 6, 7]
@@ -0,0 +1,14 @@
1+# 1. 全局统计信息 podNum, serNum, rankNum 都小于 1024
2+meta:
3+ podNum: 1
4+ serNum: 2
5+ rankNum: 6
6+ 
7+# 2. 详细拓扑结构
8+topology:
9+ - podId: 0
10+ servers:
11+ - serId: 0
12+ ranks: [0, 1]
13+ - serId: 1
14+ ranks: [0, 1, 2, 3]
@@ -0,0 +1,18 @@
1+# 1. 全局统计信息 podNum, serNum, rankNum 都小于 1024
2+meta:
3+ podNum: 1
4+ serNum: 4
5+ rankNum: 16
6+ 
7+# 2. 详细拓扑结构
8+topology:
9+ - podId: 0
10+ servers:
11+ - serId: 0
12+ ranks: [0, 1, 2, 3]
13+ - serId: 1
14+ ranks: [0, 1, 2, 3]
15+ - serId: 2
16+ ranks: [0, 1, 2, 3]
17+ - serId: 3
18+ ranks: [0, 1, 2, 3]
@@ -0,0 +1,523 @@
1+#!/bin/bash
2+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
4+# CANN Open Software License Agreement Version 2.0 (the "License").
5+# Please refer to the License for details. You may not use this file except in compliance with the License.
6+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
7+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
8+# See LICENSE in the root of the software repository for the full text of the License.
9+ 
10+# 根据集群拓扑配置文件,生成集群组网目录结构
11+# 功能:
12+# a. 解析集群YAML文件,在输出目录下创建所有超节点组网目录
13+# b. 支持两种集群配置格式:
14+# - 新格式(扁平):集群YAML直接包含server_list,每个server通过super_pod_id归属超节点
15+# - 旧格式(三层):集群YAML通过super_node_list引用超节点YAML,超节点YAML中定义server_list
16+# c. 支持两种方式获取server拓扑文件:
17+# - 如果server_topo是yaml文件,调用generate_topo_rootinfo.sh生成topo.json和rootinfo.json
18+# - 如果server_topo是目录名,从topo_bank/server_topo_bank目录拷贝
19+# d. 支持id_range扩展:集群配置中super_node_list的id_range,server_list的id_range
20+# e. 调用allocate_eid.sh脚本,统一分配server和device ports的IP地址/EID
21+# f. 生成servers_info.json文件,记录所有server的host IP和soc_version信息
22+ 
23+set -e
24+ 
25+RED='\033[0;31m'
26+GREEN='\033[0;32m'
27+YELLOW='\033[1;33m'
28+BLUE='\033[0;34m'
29+NC='\033[0m'
30+ 
31+error_exit() {
32+ echo -e "${RED}[错误] $1${NC}" >&2
33+ exit 1
34+}
35+ 
36+success_msg() {
37+ echo -e "${GREEN}[成功] $1${NC}"
38+}
39+ 
40+info_msg() {
41+ echo -e "${BLUE}[信息] $1${NC}"
42+}
43+ 
44+warn_msg() {
45+ echo -e "${YELLOW}[警告] $1${NC}"
46+}
47+ 
48+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
49+BASE_DIR="${SCRIPT_DIR}"
50+ 
51+CLUSTER_CONFIG_DIR="${BASE_DIR}/cluster_model/config/cluster"
52+SUPERPOD_CONFIG_DIR="${BASE_DIR}/cluster_model/config/super_pod"
53+SERVER_CONFIG_DIR="${BASE_DIR}/cluster_model/config/server_or_pod"
54+SERVER_TOPO_BANK_DIR="${BASE_DIR}/topo_bank/server_topo_bank"
55+GENERATE_TOPO_SCRIPT="${BASE_DIR}/generate_server_topo.sh"
56+ALLOCATE_EID_SCRIPT="${BASE_DIR}/allocate_eid.sh"
57+DEFAULT_OUTPUT_DIR="${BASE_DIR}/cluster_model/network/cluster"
58+ 
59+resolve_path() {
60+ local input="$1"
61+ local default_dir="$2"
62+ if [[ "$input" = /* ]]; then
63+ if [[ ! -f "$input" ]]; then
64+ error_exit "文件不存在: $input"
65+ fi
66+ realpath "$input"
67+ else
68+ local full_path="${default_dir}/${input}"
69+ if [[ ! -f "$full_path" ]]; then
70+ error_exit "文件不存在: $full_path (默认路径: ${default_dir})"
71+ fi
72+ realpath "$full_path"
73+ fi
74+}
75+ 
76+show_usage() {
77+ cat << EOF
78+用法: $(basename "$0") [OPTIONS] <集群配置文件>
79+ 
80+参数:
81+ 集群配置文件 配置文件路径或文件名
82+ 绝对路径: 直接使用
83+ 文件名/相对路径: 在 ${CLUSTER_CONFIG_DIR} 下查找
84+ 
85+选项:
86+ -o, --output DIR 指定输出目录 (默认: ${DEFAULT_OUTPUT_DIR})
87+ -h, --help 显示帮助信息
88+ 
89+示例:
90+ $(basename "$0") ascend950_cluster_4_server_normal.yaml
91+ $(basename "$0") /absolute/path/to/cluster_config.yaml
92+ $(basename "$0") -o /tmp/my_output ascend950_cluster_4_server_normal.yaml
93+ 
94+说明:
95+ 1. 解析集群YAML文件,自动检测配置格式(新格式/旧格式)
96+ 2. 新格式:server_list直接在集群YAML中,通过super_pod_id归属超节点
97+ 3. 旧格式:通过super_node_list引用超节点YAML,超节点YAML中定义server_list
98+ 4. 在输出目录下为每个超节点创建目录
99+ 5. 在超节点目录下为每个server创建目录
100+ 6. 根据server_topo配置生成或拷贝topo.json和rootinfo.json
101+ 7. 调用allocate_eid.sh统一分配IP/EID地址
102+ 8. 生成servers_info.json文件
103+ 
104+EOF
105+}
106+ 
107+CLUSTER_YAML_INPUT=""
108+OUTPUT_DIR=""
109+ 
110+while [[ $# -gt 0 ]]; do
111+ case $1 in
112+ -h|--help)
113+ show_usage
114+ exit 0
115+ ;;
116+ -o|--output)
117+ OUTPUT_DIR="$2"
118+ shift 2
119+ ;;
120+ *)
121+ CLUSTER_YAML_INPUT="$1"
122+ shift
123+ ;;
124+ esac
125+done
126+ 
127+if [[ -z "$CLUSTER_YAML_INPUT" ]]; then
128+ error_exit "请指定集群配置文件\n$(show_usage)"
129+fi
130+ 
131+CLUSTER_YAML_PATH="$(resolve_path "$CLUSTER_YAML_INPUT" "$CLUSTER_CONFIG_DIR")"
132+ 
133+if [[ -z "$OUTPUT_DIR" ]]; then
134+ OUTPUT_DIR="${DEFAULT_OUTPUT_DIR}"
135+fi
136+ 
137+echo "=========================================="
138+echo " 昇腾集群组网拓扑生成工具"
139+echo "=========================================="
140+echo ""
141+echo "集群配置文件: $CLUSTER_YAML_PATH"
142+echo "输出根目录: $OUTPUT_DIR"
143+echo ""
144+ 
145+CLUSTER_BASENAME=$(basename "$CLUSTER_YAML_PATH" .yaml)
146+ 
147+OUTPUT_DIR="${OUTPUT_DIR}/${CLUSTER_BASENAME}"
148+ 
149+echo "集群输出目录: $OUTPUT_DIR"
150+echo ""
151+ 
152+rm -rf "$OUTPUT_DIR"
153+mkdir -p "$OUTPUT_DIR"
154+ 
155+export CLUSTER_YAML_PATH
156+export OUTPUT_DIR
157+export CLUSTER_CONFIG_DIR
158+export SUPERPOD_CONFIG_DIR
159+export SERVER_CONFIG_DIR
160+export SERVER_TOPO_BANK_DIR
161+export GENERATE_TOPO_SCRIPT
162+export ALLOCATE_EID_SCRIPT
163+ 
164+python3 << 'PYTHON_SCRIPT'
165+import json
166+import os
167+import re
168+import shutil
169+import subprocess
170+import sys
171+import socket
172+ 
173+try:
174+ import yaml
175+except ImportError:
176+ print("[错误] 需要PyYAML库,请安装: pip install pyyaml")
177+ sys.exit(1)
178+ 
179+CLUSTER_YAML_PATH = os.environ.get('CLUSTER_YAML_PATH', '')
180+OUTPUT_DIR = os.environ.get('OUTPUT_DIR', '')
181+SUPERPOD_CONFIG_DIR = os.environ.get('SUPERPOD_CONFIG_DIR', '')
182+SERVER_CONFIG_DIR = os.environ.get('SERVER_CONFIG_DIR', '')
183+SERVER_TOPO_BANK_DIR = os.environ.get('SERVER_TOPO_BANK_DIR', '')
184+GENERATE_TOPO_SCRIPT = os.environ.get('GENERATE_TOPO_SCRIPT', '')
185+ALLOCATE_EID_SCRIPT = os.environ.get('ALLOCATE_EID_SCRIPT', '')
186+ 
187+URMA_EID_LEN = 16
188+ 
189+def ip_to_eid_string(ip, family=socket.AF_INET):
190+ if family == socket.AF_INET:
191+ ip_bytes = socket.inet_pton(socket.AF_INET, ip)
192+ eid = bytearray(URMA_EID_LEN)
193+ eid[12:16] = ip_bytes
194+ elif family == socket.AF_INET6:
195+ ip_bytes = socket.inet_pton(socket.AF_INET6, ip)
196+ eid = bytearray(URMA_EID_LEN)
197+ eid[0:16] = ip_bytes
198+ else:
199+ raise ValueError(f"不支持的地址族: {family}")
200+ hex_str = ''.join(f'{b:02x}' for b in eid)
201+ return f'0x{hex_str}'
202+ 
203+def expand_id_range(entry):
204+ ids = []
205+ if 'id' in entry:
206+ raw = entry['id']
207+ if isinstance(raw, list):
208+ ids.extend([int(x) for x in raw])
209+ else:
210+ ids.append(int(raw))
211+ if 'id_range' in entry:
212+ rng = entry['id_range']
213+ if isinstance(rng, list) and len(rng) == 2:
214+ ids.extend(list(range(int(rng[0]), int(rng[1]) + 1)))
215+ elif isinstance(rng, list):
216+ ids.extend([int(x) for x in rng])
217+ return ids
218+ 
219+def find_superpod_yaml(topo_name):
220+ if not topo_name:
221+ return None
222+ if os.path.isfile(topo_name):
223+ return topo_name
224+ candidates = [
225+ os.path.join(SUPERPOD_CONFIG_DIR, topo_name),
226+ os.path.join(SUPERPOD_CONFIG_DIR, topo_name if topo_name.endswith('.yaml') else topo_name + '.yaml'),
227+ ]
228+ for c in candidates:
229+ if os.path.isfile(c):
230+ return c
231+ return None
232+ 
233+def find_server_topo(topo_name):
234+ if not topo_name:
235+ return None, None
236+ yaml_candidates = [
237+ os.path.join(SERVER_CONFIG_DIR, topo_name),
238+ os.path.join(SERVER_CONFIG_DIR, topo_name if topo_name.endswith('.yaml') else topo_name + '.yaml'),
239+ ]
240+ for c in yaml_candidates:
241+ if os.path.isfile(c):
242+ return 'yaml', c
243+ dir_candidates = [
244+ os.path.join(SERVER_TOPO_BANK_DIR, topo_name),
245+ ]
246+ for c in dir_candidates:
247+ if os.path.isdir(c):
248+ return 'bank', c
249+ return None, None
250+ 
251+def generate_server_topo(yaml_path, output_dir):
252+ os.makedirs(output_dir, exist_ok=True)
253+ cmd = ['bash', GENERATE_TOPO_SCRIPT, yaml_path, '-o', output_dir, '-b']
254+ result = subprocess.run(cmd, capture_output=True, text=True)
255+ if result.returncode != 0:
256+ print(f" [错误] 生成server拓扑失败: {result.stderr}")
257+ return False
258+ return True
259+ 
260+def copy_server_topo_from_bank(bank_dir, output_dir):
261+ os.makedirs(output_dir, exist_ok=True)
262+ copied_files = []
263+ for f in os.listdir(bank_dir):
264+ src = os.path.join(bank_dir, f)
265+ if os.path.isfile(src):
266+ shutil.copy2(src, output_dir)
267+ copied_files.append(f)
268+ return copied_files
269+ 
270+# ============================================================
271+# Step A: 解析集群YAML文件,自动检测格式
272+# ============================================================
273+print("[信息] Step A: 解析集群配置文件...")
274+ 
275+with open(CLUSTER_YAML_PATH, 'r') as f:
276+ cluster_config = yaml.safe_load(f)
277+ 
278+cluster_name = cluster_config.get('name', 'unknown')
279+super_node_num = cluster_config.get('super_node_num', 0)
280+server_list_raw = cluster_config.get('server_list', [])
281+super_node_list_raw = cluster_config.get('super_node_list', [])
282+ 
283+is_new_format = bool(server_list_raw and not super_node_list_raw)
284+ 
285+print(f" 集群名称: {cluster_name}")
286+print(f" 超节点数量: {super_node_num}")
287+print(f" 配置格式: {'新格式(扁平)' if is_new_format else '旧格式(三层)'}")
288+ 
289+if super_node_num == 0 and not is_new_format:
290+ print("[错误] 集群配置文件中未定义超节点 (super_node_num=0)")
291+ sys.exit(1)
292+ 
293+# ============================================================
294+# Step B: 构建超节点→server映射,创建目录结构
295+# ============================================================
296+# sp_servers: {sp_id: [{'id': srv_id, 'server_topo': ..., 'soc_version': ...}, ...]}
297+sp_servers = {}
298+ 
299+if is_new_format:
300+ # 新格式:server_list直接在集群YAML中,通过super_pod_id归属超节点
301+ server_num = cluster_config.get('server_num', 0)
302+ print(f" Server数量: {server_num}")
303+ 
304+ for entry in server_list_raw:
305+ sp_id = entry.get('super_pod_id', 0)
306+ ids = expand_id_range(entry)
307+ srv_topo = entry.get('server_topo', '')
308+ soc_version = entry.get('soc_version', '')
309+ for sid in ids:
310+ if sp_id not in sp_servers:
311+ sp_servers[sp_id] = []
312+ sp_servers[sp_id].append({
313+ 'id': sid,
314+ 'server_topo': srv_topo,
315+ 'soc_version': soc_version
316+ })
317+ 
318+ actual_server_count = sum(len(v) for v in sp_servers.values())
319+ actual_super_node_count = len(sp_servers)
320+ print(f" 展开后: {actual_super_node_count} 个超节点, {actual_server_count} 个server")
321+ 
322+ if server_num > 0 and actual_server_count != server_num:
323+ print(f"[错误] server_num({server_num})与server_list展开后的实际server数量({actual_server_count})不一致")
324+ sys.exit(1)
325+ 
326+ if super_node_num > 0 and actual_super_node_count != super_node_num:
327+ print(f"[错误] super_node_num({super_node_num})与server_list中实际超节点数量({actual_super_node_count})不一致")
328+ sys.exit(1)
329+else:
330+ # 旧格式:通过super_node_list引用超节点YAML
331+ expanded_super_nodes = []
332+ for entry in super_node_list_raw:
333+ ids = expand_id_range(entry)
334+ topo = entry.get('superpod_topo', '')
335+ for sid in ids:
336+ expanded_super_nodes.append({'id': sid, 'superpod_topo': topo})
337+ 
338+ print(f" 展开后超节点数: {len(expanded_super_nodes)}")
339+ 
340+ if super_node_num > 0 and len(expanded_super_nodes) != super_node_num:
341+ print(f"[错误] super_node_num({super_node_num})与super_node_list展开后的实际超节点数量({len(expanded_super_nodes)})不一致")
342+ sys.exit(1)
343+ 
344+ for sp_entry in expanded_super_nodes:
345+ sp_id = sp_entry['id']
346+ sp_topo_name = sp_entry['superpod_topo']
347+ 
348+ sp_yaml_path = find_superpod_yaml(sp_topo_name)
349+ if not sp_yaml_path:
350+ print(f" [错误] 超节点拓扑配置文件不存在: {sp_topo_name}")
351+ continue
352+ 
353+ with open(sp_yaml_path, 'r') as f:
354+ sp_config = yaml.safe_load(f)
355+ 
356+ sp_name = sp_config.get('name', 'unknown')
357+ sp_form_type = sp_config.get('form_type', 'server')
358+ server_num = sp_config.get('server_num', 0)
359+ sp_server_list_raw = sp_config.get('server_list', [])
360+ 
361+ print(f" [信息] 超节点{sp_id}: {sp_name}, 形态: {sp_form_type}, Server数量: {server_num}")
362+ 
363+ expanded_servers = []
364+ for srv_entry in sp_server_list_raw:
365+ ids = expand_id_range(srv_entry)
366+ srv_topo = srv_entry.get('server_topo', '')
367+ soc_version = srv_entry.get('soc_version', '')
368+ for sid in ids:
369+ expanded_servers.append({
370+ 'id': sid,
371+ 'server_topo': srv_topo,
372+ 'soc_version': soc_version
373+ })
374+ 
375+ sp_servers[sp_id] = expanded_servers
376+ print(f" [信息] 展开后Server数: {len(expanded_servers)}")
377+ 
378+ if server_num > 0 and len(expanded_servers) != server_num:
379+ print(f"[错误] 超节点{sp_id}的server_num({server_num})与server_list展开后的实际server数量({len(expanded_servers)})不一致")
380+ sys.exit(1)
381+ 
382+print("")
383+ 
384+# ============================================================
385+# Step C: 创建目录结构,生成/拷贝server拓扑文件
386+# ============================================================
387+for sp_id in sorted(sp_servers.keys()):
388+ sp_dir = os.path.join(OUTPUT_DIR, f'superpod{sp_id}')
389+ os.makedirs(sp_dir, exist_ok=True)
390+ print(f"[成功] 创建超节点目录: superpod{sp_id}")
391+ 
392+ for srv in sp_servers[sp_id]:
393+ srv_id = srv['id']
394+ srv_topo_name = srv['server_topo']
395+ srv_soc_version = srv['soc_version']
396+ 
397+ srv_dir = os.path.join(sp_dir, f'server{srv_id}')
398+ 
399+ topo_type, topo_path = find_server_topo(srv_topo_name)
400+ 
401+ if topo_type == 'yaml':
402+ if generate_server_topo(topo_path, srv_dir):
403+ print(f" [成功] server{srv_id}: 生成拓扑 ({srv_topo_name})")
404+ else:
405+ print(f" [错误] server{srv_id}: 生成拓扑失败 ({srv_topo_name})")
406+ elif topo_type == 'bank':
407+ copied = copy_server_topo_from_bank(topo_path, srv_dir)
408+ print(f" [成功] server{srv_id}: 拷贝 {len(copied)} 个文件 ({srv_topo_name})")
409+ else:
410+ print(f" [错误] server{srv_id}: 找不到拓扑配置 ({srv_topo_name})")
411+ 
412+ print("")
413+ 
414+# ============================================================
415+# Step D: 调用allocate_eid.sh分配EID/IP地址
416+# ============================================================
417+print("[信息] Step D: 调用allocate_eid.sh分配EID/IP地址...")
418+ 
419+if os.path.isfile(ALLOCATE_EID_SCRIPT):
420+ result = subprocess.run(['bash', ALLOCATE_EID_SCRIPT, '-d', OUTPUT_DIR], capture_output=True, text=True)
421+ if result.returncode != 0:
422+ print(f" [警告] EID分配失败: {result.stderr}")
423+ else:
424+ print(result.stdout)
425+else:
426+ print(f" [警告] allocate_eid.sh脚本不存在: {ALLOCATE_EID_SCRIPT}")
427+ 
428+# ============================================================
429+# Step E: 生成servers_info.json
430+# ============================================================
431+print("[信息] Step E: 生成servers_info.json...")
432+ 
433+def extract_soc_version(filename):
434+ match = re.match(r'(ascend\d+)', filename)
435+ if match:
436+ return match.group(1)
437+ return 'unknown'
438+ 
439+server_ip_list = []
440+ 
441+for sp_id in sorted(sp_servers.keys()):
442+ sp_idx = sp_id
443+ for srv in sp_servers[sp_id]:
444+ srv_idx = srv['id']
445+ srv_soc_version = srv['soc_version']
446+ 
447+ if not srv_soc_version:
448+ srv_dir = os.path.join(OUTPUT_DIR, f'superpod{sp_id}', f'server{srv_idx}')
449+ soc_version = 'unknown'
450+ if os.path.isdir(srv_dir):
451+ for f in os.listdir(srv_dir):
452+ if 'rootinfo' in f and f.endswith('.json'):
453+ soc_version = extract_soc_version(f)
454+ break
455+ if soc_version == 'unknown':
456+ topo_json_path = os.path.join(srv_dir, 'topo.json')
457+ if os.path.isfile(topo_json_path):
458+ try:
459+ with open(topo_json_path, 'r') as tf:
460+ topo_data = json.load(tf)
461+ soc_version = topo_data.get('soc_version', 'unknown')
462+ except Exception:
463+ pass
464+ srv_soc_version = soc_version
465+ 
466+ aa = 192 + sp_idx
467+ bb = srv_idx + 1
468+ host_ip = f"{aa}.{bb}.0.0"
469+ host_eid = ip_to_eid_string(host_ip)
470+ 
471+ server_ip_list.append({
472+ "soc_version": srv_soc_version,
473+ "addr": host_eid
474+ })
475+ 
476+servers_info = {
477+ "version": "2.0",
478+ "server_count": len(server_ip_list),
479+ "server_ip_list": server_ip_list
480+}
481+ 
482+output_path = os.path.join(OUTPUT_DIR, 'servers_info.json')
483+with open(output_path, 'w') as f:
484+ json.dump(servers_info, f, indent=4, ensure_ascii=False)
485+ 
486+print(f" [成功] 生成servers_info.json: {output_path}")
487+print(f" 服务器数量: {len(server_ip_list)}")
488+for i, entry in enumerate(server_ip_list):
489+ print(f" [{i}] {entry['soc_version']}: {entry['addr']}")
490+ 
491+PYTHON_SCRIPT
492+ 
493+echo ""
494+echo "=========================================="
495+echo " 生成完成"
496+echo "=========================================="
497+echo ""
498+echo "输出目录: $OUTPUT_DIR"
499+echo ""
500+ 
501+TOTAL_SUPERPODS=$(find "$OUTPUT_DIR" -maxdepth 1 -type d -name "superpod*" | wc -l | tr -d ' ')
502+TOTAL_SERVERS=$(find "$OUTPUT_DIR" -mindepth 2 -maxdepth 2 -type d -name "server*" | wc -l | tr -d ' ')
503+TOTAL_FILES=$(find "$OUTPUT_DIR" -type f | wc -l | tr -d ' ')
504+ 
505+echo "超节点目录数: $TOTAL_SUPERPODS"
506+echo "Server目录数: $TOTAL_SERVERS"
507+echo "文件总数: $TOTAL_FILES"
508+ 
509+if [[ -f "${OUTPUT_DIR}/servers_info.json" ]]; then
510+ echo "servers_info.json: 已生成"
511+fi
512+ 
513+echo ""
514+ 
515+echo "目录结构:"
516+if command -v tree &>/dev/null; then
517+ tree -L 3 "$OUTPUT_DIR" 2>/dev/null || find "$OUTPUT_DIR" -type f | head -30
518+else
519+ find "$OUTPUT_DIR" -type f | head -30
520+fi
521+ 
522+echo ""
523+success_msg "集群拓扑目录生成完成!"