Build Analytical Backend
build.sh
脚本是构建过程的高级控制器。其核心职责是解析用户意图,执行预构建步骤,并以正确的参数调用底层的 CMake 工具链。
选项解析: 脚本通过
getopts
处理以下命令行标志:-t <target>
: 指定编译目标。有效值为all
,congestion_unaware
,congestion_aware
。此值将作为变量传递给 CMake。-l
: 触发清理 (cleanup
) 流程,删除所有构建产物并终止脚本。-d
: 启用调试 (Debug
) 模式进行编译。
环境准备 (
setup
,compile_chakra_et
):setup
函数负责创建用于存放中间文件和最终产物的build
目录,确保源码树的清洁。同时,它会根据系统核心数设置一个上限为 16 的并发编译线程数,以优化编译效率。compile_chakra_et
函数负责处理et_def.proto
这一 Protobuf 依赖。它检查目标文件是否存在,若不存在,则调用protoc
编译器生成相应的 C++ 和 Python 源码。
构建执行 (
compile_astrasim_analytical
,compile_astrasim_analytical_as_debug
):- 这两个函数是脚本与 CMake 交互的核心。它们根据用户是否指定
-d
标志,决定是执行标准Release
构建还是Debug
构建。关键在于它们会将用户指定的build_target
作为-DBUILDTARGET
参数传递给 CMake。
- 这两个函数是脚本与 CMake 交互的核心。它们根据用户是否指定
后处理 (
create_symlink_*
):- 编译完成后,
create_symlink_congestion_unaware
和create_symlink_congestion_aware
等函数会为生成的二进制文件创建符号链接。此举旨在维持对旧文件路径的向后兼容性。
- 编译完成后,
CMakeLists.txt
文件是项目的构建蓝图,它向 CMake 阐述了项目的结构、依赖关系以及编译规则。
编译环境设定:
cmake_minimum_required(VERSION 3.15)
: 规定了运行此配置所需的最低 CMake 版本。set(CMAKE_CXX_STANDARD 17)
和set(CMAKE_CXX_STANDARD_REQUIRED ON)
: 强制项目必须在支持 C++17 标准的编译环境中构建。
编译标志 (Compiler Flags):
- 此文件为不同的构建类型(
CMAKE_BUILD_TYPE
)定义了不同的编译器标志。 Release
(默认模式):set(CMAKE_CXX_FLAGS_RELEASE "-O3")
指示编译器进行高等级优化,以追求最大化程序性能。Debug
:set(CMAKE_CXX_FLAGS_DEBUG "...")
包含一系列用于调试的标志:-O0
: 关闭所有优化,确保编译后的代码与源码行为一致。-g
: 在可执行文件中包含调试符号,这是 GDB 等调试器工作的前提。-fsanitize=address,undefined,leak
: 启用 AddressSanitizer、UndefinedBehaviorSanitizer 和 LeakSanitizer。这些是强大的运行时诊断工具,用于捕获内存访问错误、未定义行为及内存泄漏。
- 此文件为不同的构建类型(
项目结构与依赖:
project(AstraSim_Analytical)
: 声明项目名称。add_subdirectory(...)
: 此指令是组织项目的关键。它将AstraSim
核心库、Analytical
网络后端和AstraSim_Analytical
前端等多个子模块纳入构建过程。
用户自定义选项:
set(BUILDTARGET "all" CACHE STRING ...)
: 此行定义了一个名为BUILDTARGET
的可缓存变量。这使得用户可以通过cmake -D
命令从外部注入该变量的值。此变量随后会被子目录中的CMakeLists.txt
文件用来实现条件编译。
Build ns-3 Backend
构建命令为 ./build/astra_ns3/build.sh -c
,他会执行该脚本里的 compile 函数
|
|
./ns3 configure --enable-mpi
- 参数解析 (
parse_args
): 脚本的argparse
模块会识别出configure
子命令和--enable-mpi
选项。--enable-mpi
是一个预定义的"On-Off"选项,用于控制 MPI (Message Passing Interface) 分布式仿真功能的支持。 - 进入配置步骤 (
configuration_step
): 由于检测到 configure 命令,脚本会调用configuration_step
函数。 - 调用 CMake (
configure_cmake
):configuration_step
函数内部会调用configure_cmake
. 这个函数是会动态地构建一个 cmake 命令。- 它会检测到
--enable-mpi
选项,并通过on_off_condition
函数将其转换为 CMake 变量-DNS3_MPI=ON
. - 最终组装出的命令为为
cmake -S . -B cmake-cache -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=default -DNS3_ASSERT=ON -DNS3_LOG=ON -DNS3_WARNINGS_AS_ERRORS=OFF -DNS3_MPI=ON --warn-uninitialized
- 它会检测到
- 执行配置: 脚本通过
subprocess.run()
执行这条 cmake 命令
./ns3 build AstraSimNetwork -j 12
- 参数解析 (
parse_args
): 脚本识别出build
子命令,目标AstraSimNetwork
,以及并行任务数-j 12
. 前者会被存入args.build
列表,后者会被存入args.jobs
. - 进入构建步骤 (
build_step
): 脚本检测到build
命令,并调用build_step
函数。 - 调用 CMake 构建 (
cmake_build
):build_step
函数会遍历args.build
列表中的所有目标。在这里,它会为AstraSimNetwork
这个目标调用cmake_build
函数。- cmake_build 函数会组装出一条
cmake --build
命令。 - 将目标 AstraSimNetwork 转换为
--target AstraSimNetwork
. - 将并行任务数 12 转换为
-j 12
. - 最终组装出的命令为
cmake --build cmake-cache --target AstraSimNetwork -j 12
.
- cmake_build 函数会组装出一条
Error When Building ns-3
call of overloaded ‘format(…)’ is ambiguous ❌
问题诊断 🩺
错误信息 call of overloaded ‘format(...)’ is ambiguous
的意思是,编译器在你的代码中遇到了一个名为 format
的函数调用,但它找到了多个同名的、并且参数类型都能匹配的 format
函数定义,导致编译器不知道该选择哪一个,因此产生了“歧义”(ambiguous)。
这个歧义的来源是:
std::format
(来自 C++20 标准库): 你的项目很可能正在使用支持 C++20 或更高版本的现代编译器(如 GCC 11+)。C++20 标准库引入了一个新的格式化函数std::format
。fmt::format
(来自 {fmt} 库):spdlog
这个日志库是基于一个非常流行的第三方格式化库{fmt}
构建的。这个库也提供了一个功能几乎完全相同的fmt::format
函数。在spdlog
的上下文中,它通常可以直接以format
的形式被调用。
当你的代码(这里是 spdlog_setup
的一部分)简单地调用 format(...)
时,如果 C++20 的 <format>
头文件被包含,编译器就会同时看到 std::format
和 spdlog
内部的 fmt::format
。由于两者都能处理字符串字面量 (const char[]
) 和 std::string
,编译器无法决定用哪个,从而报错。
关于 using fmt::format;
为何仍然无效的解释
原因是,除了常规的命名空间查找规则,C++ 还有一个更强大的规则叫做参数依赖查找(Argument-Dependent Lookup, ADL),有时也被称为 Koenig 查找。
我们来梳理一下编译器在看到 format(...)
这行代码时的“思考过程”:
在当前作用域查找
编译器看到了你的
using fmt::format;
声明。很好,它在当前作用域里找到了一个叫做format
的函数(也就是fmt::format
)。这成为了候选者 A。参数依赖查找 (ADL) —— 问题的根源
接下来,编译器会检查
format(...)
函数的所有参数类型。在你的错误日志里,我们看到了const std::string&
这样的参数。- ADL 规则规定:如果一个函数的参数是某个命名空间
N
下的类型(比如std::string
是std
命名空间下的),那么编译器也必须去那个命名空间N
(这里是std
) 里面去查找同名的函数。 - 由于
std::string
是std
命名空间的成员,ADL 规则被触发,编译器自动地去std
命名空间里寻找名为format
的函数。 - 因为你使用了 C++20 编译器,它在
std
命名空间里成功找到了std::format
。这成为了候选者 B。
- ADL 规则规定:如果一个函数的参数是某个命名空间
产生歧义
现在编译器陷入了困境。它手头有两个同样匹配的候选函数:
- 候选者 A:
fmt::format
(通过using
声明找到) - 候选者 B:
std::format
(通过 ADL 在参数的命名空间里找到)
using
声明只是将一个名字引入当前作用域,它并**没有足够的“特权”**去压制一个通过 ADL 找到的同样优秀的候选者。因为两个函数都能完美处理你传入的参数,编译器无法做出选择,所以它只能放弃并报告“调用是模糊的 (ambiguous)”。- 候选者 A:
结论与最终解决方案 ✅
这个 C++ 的特性意味着,只要你的函数参数中包含了 std
命名空间里的类型(如 std::string
, std::vector
等),ADL 就有可能被触发,从而把 std
里的函数(如 std::format
, std::to_string
等)也拉入候选列表,造成意想不到的冲突。
因此,唯一能 100% 消除歧义、让编译器别无选择的方法,就是使用显式的命名空间限定:
|
|
Runing Arguments
执行仿真需要传递一些参数,命令模板如下
|
|
WORKLOAD_CONFIG
astra-sim 使用的是 Chakra (Execution Trace) 作为 workload 层的输入。将 chakra 作为 python package 安装后有几个命令通过 pyproject.toml 对应到 python函数。
Explanation of toml file
pyproject.toml
是一个标准化的配置文件,用于定义 Python 项目的元数据、依赖关系以及构建和开发工具的配置。
[build-system]
构建系统配置,这部分定义了如何构建你的 Python 包。
**requires**
: 列出了构建项目本身所必需的包。这些是构建环境的依赖,而不是你代码运行时的依赖。setuptools
,setuptools-grpc
: 表明此项目使用setuptools
作为其构建工具,并需要setuptools-grpc
插件。
**build-backend**
: 指定了构建工具中实际执行构建过程的 Python 对象(入口点)。setuptools.build_meta
: 这是setuptools
提供的标准构建后端。
[project]
:这部分包含了项目的基本信息,这些信息会展示在 PyPI (Python Package Index) 上。
**name**
: 包的名称,即pip install chakra
中的chakra
。**requires-python**
: 运行此包所需的最低 Python 版本,这里是3.7
或更高。**version**
: 当前包的版本号。**readme**
: 指向一个文件,该文件的内容将作为项目在 PyPI 上的详细描述。**license**
: 指向包含许可证信息的文件。**authors**
:项目的作者信息。**dependencies**
: 项目运行时的依赖项。当用户pip install chakra
时,这些包也会被一并安装。protobuf==5.*
: 需要版本为 5.x 的protobuf
库。graphviz
,networkx
,pydot
: 其他标准的第三方库依赖。HolisticTraceAnalysis @ git+...
: 这是一个特殊的依赖。它直接从 GitHub 仓库的一个特定 commit (d731cc...
) 来安装。这确保了项目依赖于一个稳定且不会意外变动的版本。
[project.urls]
:项目相关链接,这些链接会显示在 PyPI 页面的侧边栏,为用户提供更多信息的入口。
**Homepage**
,**Documentation**
,**Repository**
: 分别指向项目主页、文档和代码仓库的 URL。
[tool.setuptools]
:这部分是针对构建工具setuptools
的详细配置。
**package-dir**
: 定义了 Python 包名与实际源代码目录之间的映射关系。- 例如,
"chakra.src.converter" = "src/converter"
表示当用户import chakra.src.converter
时,Python 会从src/converter/
目录下寻找代码。这使得项目可以使用src
布局。
- 例如,
**package-data**
: 指定需要包含在最终发布包中的非 Python 文件。"chakra.schema.protobuf" = ["et_def.proto"]
: 表示需要将et_def.proto
这个文件打包到chakra.schema.protobuf
这个包里。
[project.scripts]
:这部分定义了在安装包时应创建的命令行工具。
**chakra_converter = "chakra.src.converter.converter:main"**
: 这行配置意味着,当用户安装此包后,他们可以在终端中直接运行chakra_converter
命令。执行此命令时,系统会调用chakra.src.converter.converter
模块中的main
函数。
[tool.ruff]
:这部分是用于配置Ruff
高性能代码检查(Linter)和格式化(Formatter)工具。
**target-version**
,**line-length**
,**exclude**
: 基本配置,如目标 Python 版本、每行最大长度和要排除检查的文件。**[tool.ruff.lint]**
: Linter 的具体配置。**select**
: 启用一系列代码规则集(例如D
代表文档字符串pydocstyle
,I
代表导入排序isort
)。**ignore**
: 全局禁用的特定规则。注释中解释了忽略它们的原因(例如,规则冲突或待办事项)。**per-file-ignores**
: 针对特定文件或目录禁用规则。例如,"**/tests/*" = ["D"]
表示在所有测试文件中都禁用文档字符串检查。
**[tool.ruff.format]**
: 格式化器的配置,如使用空格作为缩进风格。
[tool.pyright]
:这部分配置了Pyright
,一个由微软开发的静态类型检查工具。
**typeCheckingMode**
: 类型检查的严格程度,这里是basic
(基础模式)。**exclude**
:在进行类型检查时要忽略的文件和目录。**report...**
:关闭特定的错误或警告报告。
[tool.vulture]
:这部分配置了Vulture
,一个用于发现项目中未使用(“死”)代码的工具。
**ignore_names**
: 让 Vulture 忽略某些特定的变量名或函数名,即使它们看起来未使用。**min_confidence**
: 设置报告问题的最低置信度阈值。100
表示只有在 Vulture 100% 确定代码是无用的时候才会报告,这可以有效减少误报。
|
|
Generate Execution Trace
ASTRA-sim 的 ET 命名格式为 {path prefix/trace name}.{npu_id}.et
. Chakra ET 的获取流程如下图所示1。
- Collect ET from PyTorch
- PyTorch ET 负责 CPU 算子,并明确表示它们之间的依赖关系。
- Kineto Trace 编码 GPU 算子及其开始和结束时间。
- Merge Trace by
chkra_trace_link
:将它们合并为一个 PyTorch ET+. 该格式本质上遵循 PyTorch ET 的模式,但同时也编码了 GPU 操作符及其依赖关系。 - Convert to Chakra ET by
chakra_converter
Overview of Trace Collection
具体的教程和例子可以在 Conversion Guide 和 Practical Example 找到。
Using ET Converter
可以将 astra-sim 1.0 的文本输入转换成 Chakra ET.
|
|
workload 文本格式要求如下,其中通信大小单位是字节,计算时间以周期数表示。
- 第一行:(DATA/HYBRID_TRANSFORMER/HYBRID_DLRM)
- 该行指定训练循环的并行化类型。DATA 表示纯数据并行方法,HYBRID_TRANSFORMER 表示专为 Transformer DNN 网络设计的混合并行方法,而 HYBRID_DLRM 表示专为 DLRM DNN 网络优化的混合并行方法。
- 第二行:(int)
- 该行表示 DNN 的层数。
- 后续行:每行描述一层。层的描述格式如下:
- {(string: 层名称)
- (int: 保留变量)
- (int: 前向计算时间)
- (ALLREDUCE/ALLGATHER/ALLTOALL: 前向通信类型)
- (int: 前向通信大小)
- (int: 输入梯度计算时间)
- (ALLREDUCE/ALLGATHER/ALLTOALL: 输入梯度通信类型)
- (int: 输入梯度通信大小)
- (int: 权重梯度计算时间)
- (ALLREDUCE/ALLGATHER/ALLTOALL: 权重梯度通信类型)
- (int: 权重梯度通信大小)
- (集合通信完成后,权重/输入/输出更新的延迟)}`
Note
每一层的参数写要在同一行!!!
Enable Communicator Groups
astra-sim 2.0 支持通信组。可以通过指定 --comm-group-configuration
JSON 文件来指定,默认只有一个通信组。
{
// The first/second communicator group, with ID 0/1, includes GPU IDs from 0-3/4-7.
// "0": [0, 1, 2, 3],
// "1": [4, 5, 6, 7]
"<communicator_group_id>" : [gpu_ids]
}
SYSTEM_CONFIG
System Layer
Workload 层会遍历 Chakra ET 中的节点,并为每个节点所指代的操作发出相应的命令。System 层接收这些命令,并将其转换为适合网络、计算或内存后端的格式,从而正确模拟操作。根据操作的类型,系统层的行为会有所不同,具体如下:
- 计算操作:向计算后端发出调用,以模拟操作的持续时间。
- 内存操作: 内存
- 通信操作:将集合通信分解为点对点的发送和接收消息,并向网络后端发出“发送”或“接收”调用,以模拟消息的传输过程。
Collective Scheduler
每个队列都有许多 StreamBaseline
对象 (图中右上角),代表了整个集合通信的流程,phase_to_go
是一个用于表示这些阶段的队列,my_current_phase
是指向当前执行阶段的指针。
|
|
对于每个 stream proceed_to_next_vnet_baseline
(astra-sim/system/Sys.cc) 用于推进通信阶段并且负责在队列之间移动 stream 对象。以下几种情况会调用该函数:
- stream 第一次被移动出 ready_list 并且将被插入到
active_streams
. - stream 完成了一个通信阶段并且等待下一个阶段。
- stream 完成了所有的通信阶段。
(2-1) 到 (2-5) 描述了该函数的行为
查看当前持有 stream 的队列: 从队列中删除
StreamBaseline
对象 (流的完成顺序可能与它们开始执行的顺序不同)。修改
StreamBaseline
对象: 已完成的集合通信阶段从phases_to_go
中弹出,my_current_phase
现在指向下一个待执行的阶段。使用
insert_stream
将StreamBaseline
对象插入到下一个队列中。调用函数
notify_stream_removed
函数查看前一个队列的头部。stream_pointer
指向队列中第一个未运行的 stream (标记为蓝色)。该函数通过调用StreamBaseline::init()
来启动 stream 的下一个阶段的执行。使用
notify_stream_added
触发新队列头部 stream 的通信阶段执行。
在其他情况下,proceed_to_next_vnet_baseline
会执行上述步骤的一部分。具体如下:
刚从
ready_list
中移除:proceed_to_next..
会初始化 stream (1-2),将其插入到第一个队列中 (1-3),并触发该队列头部的流执行。stream 完成:
该函数会从之前的队列中删除 stream (3-1),并触发之前队列头部的 stream 执行。此外,StreamBaseline
对象会被删除,并调用notify_stream_finished
,以通知Sys
对象 stream 已经结束 (3-6)
Collective Implementation
自 2024 年 8 月以来,ASTRA-sim 支持了一种新的集合通信算法表示方式。System 层通过暴露一个集体 API,可以接收任意集体算法的定义。
这两种方法都是对 CollectivePhase::Algorithm
对象的实现,该对象是 System 层中的调度单元. generate_collective_phase 会根据不同的算法在创建 CollectivePhase 的时候传入对应的 Algorithm.
ASTRA-Sim Native Implementation
相关的实现都位于该文件夹下, naive 实现的限制是当需要模拟一个新的集合通信算法时算法,必须实现整个集合?随着不规则集合通信 (如 TACOS(Topology Aware CollectiveS), MSCCLang(基于 DSL)) 中工作的增加,快速模拟和迭代各种算法的需求变得越来越多。
Chakra Based Arbitrary Definition Through Collective API
因此一个新的 AP来接受任何集合通信算法的定义,而不局限于预定义的规则通信模式。对于通信表示,使用 Chakra ET 模式作为单独的图。将集合通信算法表示为Chakra ET 模式中 COMM_SEND,COMM_RECV 节点的图。也就是说,System 层不是将集合通信分解为发送和接收消息,而是简单地遵循 Chakra 图中已经表示的分解。由于已经使用 Chakra ET 来表示 workload,使用 Chakra ET 来额外定义集合通信算法提供了一种轻松简单的方式来遍历整个图。
如上图所示当 workload 层发出 AllReduce 集体操作时,System 层不会运行模拟器代码库中已有的原生实现逻辑,而是会遍历通过 API 提供的 Chakra ET,该 ET 表示集合通信算法。需要注意 workload Chakra 图和集合通信算法的 Chakra 图是解耦的,并通过不同的输入点提供。最终,asytra-sim 模拟器会将通信节点替换为集体实现。
Input Files for Collective API
ASTRA-sim Native
// ...
"active-chunks-per-dimension": 1,
"all-reduce-implementation": ["ring"],
"all-gather-implementation": ["ring", "doubleBinaryTree"],
"all-to-all-implementation": ["ring", "doubleBinaryTree", "halvingDoubling"],
// ...
all-*-implementation
指定了模拟器将如何将给定的集合通信分解为发送和接收消息。All-Gather 操作列表中的两个条目表示模拟器将按两个维度分解 ——第一个维度使用 Ring 算法,第二个维度使用 doubleBinaryTree 算法。
Native Implementation Requires That the Dimensions for Collective Algorithms Are Same Across All Collectives.
Warning
Native 实现要求所有集体操作的维度必须相同。换句话说,如果一个集合通信算法被定义为二维的,那么其他集合通信算法也必须是二维操作。上述只是一个例子。
Collective API
// ...
"active-chunks-per-dimension": 1,
"all-reduce-implementation-chakra": ["/app/hoti2024/demo5/inputs/custom_ring"],
// ...
需要注意这里要使用 all-*-implementation-chakra
,而不是 all-*-implementation
. 另外 Chakra ET 文件与传递给 workload 层的文件是不同的,每一项的值是 Chakra ET 文件的绝对路径,不包括最后的 {rank}.et
字符串 (类似于 Workload 层输入)。此外,即使有许多维度,列表也只接受一个值。这是因为跨维度通信的概念已经包含在 ET 中。
Network Backend
Analytical Network Backend
Analytical Network 模拟器通过数学方程模拟所有网络行为。因此,该后端最适合于大规模分布式平台的建模和仿真。目前支持两种分析模式
- congestion_unaware analytical network simulator
- congestion_aware analytical network simulator
- TTopology
Analytical Network 支持三种拓扑结构: Ring, FullConnected, Switch. 并且可以堆叠来表示多维网络。
topology: [ Ring, Switch ] # 2D topology
topology: [ Ring, Ring, Ring ] # 3D topology
- NPUs Count
指定了每个维度上的设备数目
npus_count: [ 5 ] # 5 NPUs
npus_count: [ 4, 2 ] # 4 × 2 = 8 NPUs
npus_count: [ 4, 2, 2 ] # 4 × 2 × 2 = 16 NPUs
- Bandwidth & Latency
latency
定义了每条单向链路的延迟 (ns).
bandwidth
定义了每条单向链路的带宽 (GB/s).
Note
$1 GB = 2^{30} B$ and $1 s = 10^9 ns$
ns3 backend
下面是用 ns3 后端进行方针的一个执行命令。这里使用了 --network-backend
和 --logical-topology
这两个参数。需要说明的是,Analytical Backend 中仅使用了--network-backend
参数,这是因为分析型后端的逻辑拓扑与物理拓扑是相同的,而 ns3 则允许我们将逻辑拓扑与物理拓扑分离。
# {NS3_DIR} is the directory of the ns-3 backend. That is, '{ASTRA_SIM_ROOT_DIRECTORY}/extern/network_backend/ns-3'
cd "${NS3_DIR}/build/scratch"
./ns3.42-AstraSimNetwork-default \
--workload-configuration="${SCRIPT_DIR:?}"/../../extern/graph_frontend/chakra/one_comm_coll_node_allgather \
--system-configuration="${SCRIPT_DIR:?}"/../../inputs/system/Switch.json \
--network-configuration="../../../ns-3/scratch/config/config.txt" \
--remote-memory-configuration="${SCRIPT_DIR:?}"/../../inputs/remote_memory/analytical/no_memory_expansion.json \
--logical-topology-configuration="${SCRIPT_DIR:?}"/../../inputs/network/ns3/sample_8nodes_1D.json \
--comm-group-configuration=\"empty\"