1. 版本历史

文档版本

版本

作者

V3.0

ECCL 用户使用手册

Enflame Tech

2. ECCL 简介

燧原公司集合通信库(Enflame Collective Communications Library, 简称为 ECCL)在基于公司自研的芯片 GCU 上提供了卡间通信的功能。

其主要提供了以下多个通信原语:

  • ecclAllReduce:将多卡上的数据进行规约操作,并将得到的结果保存在每张卡上

  • ecclBroadcast:将一张卡上的数据广播给多张卡

  • ecclReduceScatter:将多卡上的数据进行规约,并将得到的结果均匀分片后分发到每张卡上

  • ecclAllGather:将多卡上的数据聚合一起,每张卡上的原始数据大小需一致,并将得到的结果保存在每张卡上

ECCL 同样支持点对点通信功能:

  • ecclSend:将卡上的数据发送给另一张指定的卡

  • ecclRecv:从指定的卡上接收数据到本卡

3. ECCL 用户使用手册

3.1. ECCL 使用前置准备

  • TopsPlatform,使用燧原公司提供《topsplatform 安装说明手册》进行安装

3.2. ECCL 安装

安装 ECCL 提供的 deb 包(rpm 包):

dpkg -i eccl_*_amd64.deb (rpm -ivh eccl-*.x86_64.rpm)

确认是否安装成功, 请执行:

ls /usr/include/ | grep eccl.h

检查 “eccl.h” 头文件是否存在于目录 /usr/include/ 下

ls /usr/lib/ | grep libeccl.so

检查 “libeccl.so” 是否存在于目录 /usr/lib/ 下

3.3. ECCL 使用方式

以下内容描述了如何使用 ECCL 进行集合通信操作。包括了 ECCL 接口描述、使用概述以及使用示例介绍

ECCL 对外接口

描述了 ECCL 集合通信库的对外接口以及相关的参数信息。包括了通信域生成与管理类接口、通信算子类接口、组操作接口以及点对点通信接口。详情可见《ECCL API 参考》文档。

ECCL 使用

ECCL 支持多种启动方式:

  • 多进程启动,每个进程对应单张 GCU 卡

  • 单进程多线程启动,每个线程对应单张 GCU 卡,详细使用方法可以参考 test_single_process_multi_thread sample 用例

  • 单进程单线程启动,单个线程对应多张 GCU 卡,详细使用方法可以参考 test_single_process_single_thread sample 或者 test_commInitAll 用例(test_commInitAll 用例展示了单线程初始化多卡接口 ecclCommInitAll 的使用方法)

下列代码将会展示如何使用 MPI 启动多进程,每个进程对应单张 GCU 卡,创建一个通信域并在通信域上运行集合通信算子。

  • 检索有关线程的 MPI 信息:

int myRank, nRanks;
MPI_Comm_rank(MPI_COMM_WORLD, &myRank);
MPI_Comm_size(MPI_COMM_WORLD, &nRanks);
  • 选择任一个rank生成一个通信域的 UniqueId,并把 Id 广播给域内其余的卡:

ecclUniqueId id;
if (myRank == 0) ecclGetUniqueId(&id);
MPI_Bcast((void *)&id, sizeof(id), MPI_BYTE, 0, MPI_COMM_WORLD);
  • 根据以上信息可以生成一个通信域:

ecclComm_t comm;
ecclCommInitRank(&comm, nRanks, id, myRank);
  • 在生成的通信域上调用相应的通信算子进行集合通信(以 ecclAllReduce 为例):

ecclAllReduce(..., comm);
  • 集合通信结束时,释放相应的资源并摧毁通信域:

ecclCommDestroy(comm);

组操作

组操作(ecclGroupStart/ecclGroupEnd)可以被用来将多次调用合并为单次调用。当前 ECCL 仅支持在同一个通信域中聚合相同的通信操作以提高性能。

组操作使用示例

聚合通信操作意味着在单次任务下发中,将执行多次通信操作(当前仅支持聚合多个相同通信操作)。组操作对于减少下发开销效果明显,因为其在执行多个通信操作中仅会下发一次。

对通信操作进行聚合可以通过在 ecclGroupStart 和 ecclGroupEnd 区域内多次调用 ECCL 来完成,下列代码将会展示如何在单次下发中聚合多次 AllReduce 操作:

ecclGroupStart();
for (int i = 0; i < nlayers; ++i) {
  ecclAllReduce(sendBuffs[i], recvBuffs[i], count, dataType, redOp,
                comm, topsStream);
}
ecclGroupEnd();

点对点通信

点对点通信可以指定通信域内任意两张卡之间进行数据接发。该通信方式由两个举动组成:发送端调用 ecclSend(),相应的接收端调用 ecclRecv()。此时两端传入的数据类型和位数参数需要一致。

将针对不同节点的多次点对点通信调用跟组操作结合在一起,能够完成更加复杂的通信操作,下列代码将会展示如何在单次下发中聚合多次点对点通信:

ecclGroupStart();
for (int r=0; r<nlayers; r++) {
  ecclSend(sendbuff[r], sendcount, sendtype, peer, comm, topsStream);
  ecclRecv(recvbuff[r], recvcount, recvtype, peer, comm, topsStream);
}
ecclGroupEnd();

Attention

点对点通信中发送端和接收端需要严格对应,即发送端发送的次数要等于接收端接收的次数

运行 ECCL sample

ECCL 软件包安装完成后,对应的 sample 会被一并安装至 /usr/src/eccl_samples 目录下。

此次提供了下列多个 demo sample:

  • test_sendrecv

该 demo 提供了单次点对点通信的测试方法,编译命令如下:

cd test_sendrecv && make

运行单机4卡测试命令如下, 由参数np指定参与的卡数:

mpirun -np 4 ./test_sendrecv
  • test_group_sendrecv

该 demo 提供了组操作下点对点通信的测试方法,编译命令如下:

cd test_group_sendrecv && make

运行单机4卡测试命令如下, 由参数np指定参与的卡数:

mpirun -np 4 ./test_group_sendrecv
  • test_single_process_single_thread

该 demo 提供了单进程单线程模式下集合通信的测试方法,编译命令如下:

cd test_single_process_single_thread && make

运行测试命令如下(测试内部指定运行的卡数,此测试运行卡数为4):

./test_single_process_single_thread
  • test_single_process_multi_thread

该 demo 提供了单进程多线程模式下集合通信的测试方法,编译命令如下:

cd test_single_process_multi_thread && make

运行测试命令如下(测试内部指定运行的卡数,此测试运行卡数为4):

./test_single_process_multi_thread
  • test_commInitAll

该 demo 提供了单线程模式下使用 ecclCommInitAll 接口进行初始化并进行集合通信的测试方法,编译命令如下:

cd test_commInitAll && make

运行测试命令如下(测试内部指定运行的卡数,此测试运行卡数为4):

./test_commInitAll

ECCL 使用示例

以下代码描述了一个完整的工作示例,其中包含多个 MPI 进程,每个进程管理一个 GCU 设备。

示例代码可以使用”gcc”编译器进行编译。需要在编译时链接下列的库:

  • Openmpi,推荐版本4.0.5。

  • ECCL

  • topsruntime

#include <stdio.h>
#include "tops/tops_runtime.h"
#include "eccl.h"
#include "mpi.h"
#include <unistd.h>
#include <stdint.h>
#include <stdlib.h>

#define MPICHECK(cmd) do {                          \
  int e = cmd;                                      \
  if( e != MPI_SUCCESS ) {                          \
    printf("Failed: MPI error %s:%d '%d'\n",        \
        __FILE__,__LINE__, e);   \
    exit(EXIT_FAILURE);                             \
  }                                                 \
} while(0)

#define TOPSCHECK(cmd) do {                         \
  topsError_t e = cmd;                              \
  if( e != topsSuccess ) {                          \
    printf("Failed: Tops error %s:%d '%s'\n",       \
        __FILE__,__LINE__,topsGetErrorString(e));   \
    exit(EXIT_FAILURE);                             \
  }                                                 \
} while(0)


#define ECCLCHECK(cmd) do {                         \
  ecclResult_t r = cmd;                             \
  if (r!= ecclSuccess) {                            \
    printf("Failed, ECCL error %s:%d '%d'\n",       \
        __FILE__,__LINE__,r);   \
    exit(EXIT_FAILURE);                             \
  }                                                 \
} while(0)


static uint64_t getHostHash(const char* string) {
  // Based on DJB2a, result = result * 33 ^ char
  uint64_t result = 5381;
  for (int c = 0; string[c] != '\0'; c++){
    result = ((result << 5) + result) ^ string[c];
  }
  return result;
}

static void getHostName(char* hostname, int maxlen) {
  gethostname(hostname, maxlen);
  for (int i=0; i< maxlen; i++) {
    if (hostname[i] == '.') {
        hostname[i] = '\0';
        return;
    }
  }
}

int main(int argc, char* argv[])
{
  int size = 32*1024*1024;

  int myRank, nRanks, localRank = 0;

  // 初始化MPI
  MPICHECK(MPI_Init(&argc, &argv));
  MPICHECK(MPI_Comm_rank(MPI_COMM_WORLD, &myRank));
  MPICHECK(MPI_Comm_size(MPI_COMM_WORLD, &nRanks));

  // 基于hostname计算localRank, localRank用于指定使用哪个GCU
  uint64_t hostHashs[nRanks];
  char hostname[1024];
  getHostName(hostname, 1024);
  hostHashs[myRank] = getHostHash(hostname);
  MPICHECK(MPI_Allgather(MPI_IN_PLACE, 0, MPI_DATATYPE_NULL, hostHashs, sizeof(uint64_t), MPI_BYTE, MPI_COMM_WORLD));
  for (int p=0; p<nRanks; p++) {
     if (p == myRank) break;
     if (hostHashs[p] == hostHashs[myRank]) localRank++;
  }

  ecclUniqueId id;
  ecclComm_t comm;
  float *sendbuff, *recvbuff;
  topsStream_t s;

  // 获取rank 0的ECCL unique ID 然后广播到其他rank
  if (myRank == 0) ECCLCHECK(ecclGetUniqueId(&id));
  MPICHECK(MPI_Bcast((void *)&id, sizeof(id), MPI_BYTE, 0, MPI_COMM_WORLD));

  // 依据localRank选择一个GCU, 分配资源
  TOPSCHECK(topsSetDevice(localRank));
  TOPSCHECK(topsMalloc(&sendbuff, size * sizeof(float)));
  TOPSCHECK(topsMalloc(&recvbuff, size * sizeof(float)));
  TOPSCHECK(topsStreamCreate(&s));

  // 初始化ECCL
  ECCLCHECK(ecclCommInitRank(&comm, nRanks, id, myRank));

  // 使用ECCL进行集合通信
  ECCLCHECK(ecclAllReduce((const void*)sendbuff, (void*)recvbuff, size, ecclFloat, ecclSum,
        comm, s));

  // stream 同步ECCL集合操作
  TOPSCHECK(topsStreamSynchronize(s));

  // 释放空间
  TOPSCHECK(topsFree(sendbuff));
  TOPSCHECK(topsFree(recvbuff));

  // 销毁 ECCL
  ecclCommDestroy(comm);

  // 销毁MPI
  MPICHECK(MPI_Finalize());

  printf("[MPI Rank %d] Success \n", myRank);
  return 0;
}

3.4. ECCL 环境变量

ECCL_ALGO

用于设置 ECCL 使用的算法,目前支持 Ring, Mesh 和 MeshDirect 三种算法。

该环境变量会影响分布式的性能,建议用户不额外设置此环境变量,ECCL会自适应选择合适的算法类型。

允许传入的值

  • Ring

  • Mesh

  • MeshDirect

ECCL_DEBUG

该变量用于设置 ECCL 打印的 log 级别,WARN、INFO 两种级别。WARN 级别在业务逻辑出现异常时打印,INFO 级别用于用户行为留痕、调用链跟踪。其中 INFO 需与定义子模块的环境变量 ECCL_DEBUG_SUBSYS 配合使用,WARN 会打印所有子模块。

允许传入的值

  • WARN:打开 WARN 级别 log

  • INFO:打开 INFO 级别 log

使用示例

打印 INFO 级别的初始化以及环境变量相关 log (即默认值):

export ECCL_DEBUG=INFO
export ECCL_DEBUG_SUBSYS=INIT,ENV

打印 WARN 级别的所有 log:

export ECCL_DEBUG=WARN
export ECCL_DEBUG_SUBSYS=ALL

ECCL_DEBUG_SUBSYS

用于设置需要打印的子模块,主要包括初始化、环境变量、调用等等子模块,可以设置多个子模块,多个子模块间用”,”隔开,默认值为”INIT,ENV”。关闭子模块,在子模块前加”^”。ECCL_DEBUG 设置为 INFO 时需与 ECCL_DEBUG_SUBSYS 配合使用。

允许传入的值

  • INIT:初始化阶段信息,默认值

  • ENV:环境变量信息,默认值

  • CALL:ECCL 对外接口调用信息

  • ALLOC:ECCL 内部资源分配信息

  • TRANSPORT:ECCL 内部 transport 信息

  • NET:网络相关信息,调用 socket、ibverbs 接口信息

  • TOPO:ECCL 内部拓扑相关信息

  • P2P:ECCL 点对点通信相关信息

  • ALL:ECCL 所有模块信息

使用示例

打印 INFO 级别的初始化以及环境变量相关 log:

export ECCL_DEBUG=INFO
export ECCL_DEBUG_SUBSYS=INIT,ENV

打印 WARN 级别的所有 log:

export ECCL_DEBUG=WARN
export ECCL_DEBUG_SUBSYS=ALL

关闭初始化模块打印:

export ECCL_DEBUG_SUBSYS=^INIT

ECCL_TOPO_DUMP_FILE

给定一个存储XML文件的路径,用于转存探测的物理拓扑链接。

允许传入的值

一个指向某文件的路径,这个文件可能会被创建,或者会被覆盖原有内容。

ECCL_GRAPH_DUMP_FILE

给定一个存储XML文件的路径,用于转存搜索graph的信息。

允许传入的值

一个指向某文件的路径,这个文件可能会被创建,或者会被覆盖原有内容。

4. eccl-tests

用来测试检查ECCL操作的性能和正确性。

4.1. 环境构建

eccl-tests安装包依赖eccl安装包,在安装eccl-tests之前需要先安装eccl包,见本文档 ECCL安装 部分。

eccl-tests安装

dpkg -i eccl-tests_*_amd64.deb (rpm -ivh eccl-tests-*.x86_64.rpm)

确认是否安装成功, 请执行:

ls /usr/local/bin/ | grep eccl

检查 eccl_all_reduce_perf eccl_all_gather_perf eccl_broadcast_perf eccl_reduce_perf eccl_reduce_scatter_perf 可执行文件是否存在于目录 /usr/local/bin 下

4.2. 测试使用

eccl-tests运行在多张GCU卡上,当前支持通过mpirun命令启动程序并指定启动的进程的个数,每个进程对应着一个唯一rank。目前支持一个进程管理一块GCU, 在一个进程上有一个线程。

4.3. 使用方法

eccl-tests可以用mpirun中的参数np来控制参与的卡数,并支持多种通信算子的性能测试,包括AllReduce、Broadcast、ReduceScatter、AllGather、Reduce。当前集合通信只支持单机, 不支持多机。

下面提供了测试各种通信算子的命令格式。

AllReduce

单机8卡:

mpirun -np 8 eccl_all_reduce_perf -b 1k -e 128M -f 2

Broadcast

单机8卡:

mpirun -np 8 eccl_broadcast_perf -b 1k -e 128M -f 2

ReduceScatter

单机8卡:

mpirun -np 8 eccl_reduce_scatter_perf -b 1k -e 128M -f 2

AllGather

单机8卡:

mpirun -np 8 eccl_all_gather_perf -b 1k -e 128M -f 2

Reduce

单机8卡:

mpirun -np 8 eccl_reduce_perf -b 1k -e 128M -f 2

4.4. 命令参数

Number of GCUs

表 4.4.2 GCU及线程数量

参数名称

使用方法

默认值

参数说明

-t

--nthreads <num threads>

1(仅支持1)

运行在每一个进程上的线程数

-g

--ngcus <GCUs per thread>

1(仅支持1)

运行在每一个线程上的GCU个数

Attention

目前仅支持一个进程对应一个GCU启动

Sizes to scan

表 4.4.3 需要扫描的尺寸

参数名称

使用方法

默认值

参数说明

-b

--minbytes <min size in bytes>

32M

启动时的最小尺寸

-e

--maxbytes <max size in bytes>

32M

结束时的最大的尺寸

-i

--stepbytes <increment size>

1M

在一定的尺度等量选择增量

-f

--stepfactor <increment factor>

不使用

乘法的使用

Attention

-i和-f只使用一个,默认使用-i

ECCL operations arguments

表 4.4.4 ECCL operations argument

参数名称

使用方法

默认值

参数说明

-o

--op <sum/prod/min/max/all>

sum

指定要执行的规约操作类型。仅与AllReduce、Reduce或ReduceScatter等源操作相关

-d

--datatype <eccltype>

float

指定数据类型

-r

--root <root/all>

0

指定 root GCU,应用于Broadcast或者Reduce相关操作

Attention

-d 当前只支持 int8, uint8, int32, uint32, float

Performance

表 4.4.5 Performance

参数名称

使用方法

默认值

参数说明

-n

--iters <iteration count>

20

循环迭代次数

-w

--warmup_iters <warmup iteration count>

5

预热迭代次数(不是时间)

-m

--agg_iters <aggregation count>

1

每次迭代中聚合在一起的操作数

-a

--average <0/1/2/3>

1

报告所有rank的平均性能(仅MPI=1)<0=Rank0,1=Avg,2=Min,3=Max>

Test operation

表 4.4.6 Test operation

参数名称

使用方法

默认值

参数说明

-c

--check <0/1>

1

检查结果的正确性。在大量GCU上可能较慢,默认检查

-z

--blocking <0/1>

0

进行ECCL集合通信阻塞,即让CPU在每次集体阻塞后等待和同步

4.5. 结果

表 4.5.1 eccl-tests性能相关名词解释

参数名称

参数说明

size

数据的大小(S)

count

数据的计数

type

数据的类型

redop

操作的类型

time

操作的执行时间(t)

algbw

算法带宽,根据数据大小以及操作执行时间计算得到,algbw=S/t

busbw

总线带宽,根据数据链路上实际传输的数据大小以及操作执行时间计算得到。实际是根据操作类型,将算法带宽乘以一个系数,详见下表

表 4.5.2 不同操作类型总线带宽与算法带宽的关系

操作的类型

计算关系

AllReduce

busbw=algbw*2*(n-1)/n

ReduceScatter

busbw=algbw*(n-1)/n

AllGather

busbw=algbw*(n-1)/n

Broadcast

busbw=algbw

Reduce

busbw=algbw

4.6. 注意事项

打开调试开关(ECCL_DEBUG,ECCL_DEBUG_SUBSYS)可能导致eccl-tests测得的性能低于实际性能,建议在进行eccl-tests性能测试时不要设置调试相关环境变量。