0%

https://github.com/open-mpi/ompi/issues/6691#issuecomment-497245597

ess_hnp_module.c

1
2
3
4
orte_set_attribute(&transports, ORTE_RML_TRANSPORT_TYPE, ORTE_ATTR_LOCAL, orte_mgmt_transport, OPAL_STRING);
orte_mgmt_conduit = orte_rml.open_conduit(&transports);
orte_set_attribute(&transports, ORTE_RML_TRANSPORT_TYPE, ORTE_ATTR_LOCAL, orte_coll_transport, OPAL_STRING);
orte_coll_conduit = orte_rml.open_conduit(&transports);

rml: resource message layer

根据 attr 选择 rtmod, 返回的为 mod 在 array 中的 index

Open conduit - call each component and see if they can provide a
conduit that can satisfy all these attributes - return the conduit id
(a negative value indicates error)

rml_base_stubs.c

1
orte_rml_API_open_conduit

遍历 active 的 rml mod, 调用各个 rml mod 的 open_conduit, 例如 oob (rml_oob_component.c), 返回 mod 之后,存入 array,返回 array index

1
2
3
open_conduit()

orte_get_attribute(attributes, ORTE_RML_TRANSPORT_TYPE, (void**)&comp_attrib, OPAL_STRING)

从 attr 里获取 key 值,即 orte_mgmt_transport or orte_coll_transport, 确定是否指定了 oob

继续获取 routed mod

1
orte_get_attribute(attributes, ORTE_RML_ROUTED_ATTRIB, (void**)&comp_attrib, OPAL_STRING);

根据设置的 routed mod (NULL 则按优先级) 分配 routed mod

1
md->routed = orte_routed.assign_module(comp_attrib);

orte_mca_params.c

1
2
3
4
5
6
7
8
9
10
11
orte_mgmt_transport = "oob" // default

--mca orte_mgmt_transport

ORTE management messages

orte_coll_transport = "fabric,ethernet" // default

--mca orte_coll_transports

ORTE collectives

plm_base_launch_support.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
param = NULL;
if (ORTE_SUCCESS != (rc = orte_regx.nidmap_create(orte_node_pool, &param))) {
ORTE_ERROR_LOG(rc);
return rc;
}
if (NULL != orte_node_regex) {
free(orte_node_regex);
}
orte_node_regex = param;
/* if this is too long, then we'll have to do it with
* a phone home operation instead */
if (strlen(param) < orte_plm_globals.node_regex_threshold) {
opal_argv_append(argc, argv, "-"OPAL_MCA_CMD_LINE_ID);
opal_argv_append(argc, argv, "orte_node_regex");
opal_argv_append(argc, argv, orte_node_regex);
/* mark that the nidmap has been communicated */
orte_nidmap_communicated = true;
}

orte_node_pool

node_regex_threshold = 1024 default len

Resource Allocation Subsystem (RAS)

managing python version

1
conda create --name dl python=3.6

activate your env [dl]

1
2
conda info --envs
conda activate dl

PyCharm with anaconda

build the latest openmpi

1
2
3
4
5
6
# download the openmpi-v4.0.0.tar.gz from the official website
# untar and run configure
./configure --prefix=/usr/local/openmpi --enable-orterun-prefix-by-default
# make and install
make -j $(nproc) all
make install

verify openmpi

1
mpirun -np 4 --bind-to none --map-by slot hostname

install tensorflow

1
/anaconda3/envs/dl/bin/pip --proxy http://127.0.0.1:1081 install tensorflow==1.13.1

install horovod

1
HOROVOD_WITH_TENSORFLOW=1 /anaconda3/envs/dl/bin/pip install -v --no-cache-dir horovod==0.16.2

ref horovod Dockerfile

0.16.2

horovod 启动流程

hvd.init() 发生了什么

HorovodBasics -> init -> horovod_init -> InitializeHorovodOnce

启动线程

BackgroundThreadLoop

MPI 初始化

MPI_Comm_dup

MPI_Comm_rank 获取当前进程的 RANK

MPI_Comm_size 获取 local size

两次 AllGather, 把 rank 与 size 分发到所有进程

检查是否同构,即 size 是否相同

rank 0 初始化 MessageTable

initialization_done = true 初始化结束

主线程等待

initialization_done = true

背景线程持续 RunLoopOnce

1
while (RunLoopOnce(state, is_coordinator));

从 message_queue 中获取数据

DistributedOptimizer

MXNet 分布式通信

MXnet 使用了 dmlc/ps-lite 实现其分布式通信机制

ps-lite

van.h 父类,使用 Protocol buffer 定义了数据格式

zmq_van.h 继承 van.h,使用 zmq 做具体的数据传输

当前 MXnet 只能使用 IPoIB,不能直接使用 IB verbs 来通信;有个改动 ps-lite 也能直接使用 ib 来通信的 PR 挂了一年多了 …

RDMA in kubernetes

IB 这东西嘛,看起来是没服务发现的能力的;就是使用之前,需要使用 IP 网络通信一次,然后知道对端的 IB 的地址后,才能直接基于 IB 来通信

NCCL 就是这么干的

当然参考阿里云的说法

RDMA通讯建连过程普遍分为 TCP 和 RDMA_CM 两种实现,如果应用程序使用RDMA_CM 的建连的方式,vpc网络插件中分配的pod ip 无法作为RDMA_CM 地址, 容器需要设置HostNetwork。并将bond0 ip 设置为CM的通讯地址

这得看下 mlx device plugin 的实现和容器网络的实现,比较好奇,看一下吧

业界有两种实现 SR-IOV (single root io virtualization) 另一种就是直接共享设备 (HCA: host channel adapter)

HCA

HCA 就没啥好说的了,device plugin allocate 时,给 kubelet 返回设备路径,直接整个把 /dev/infiniband 挂载入容器中即可 … 这点会比较误导,相当于可以用所有的 HCA ?

所以对于 HCA 来说,直接用原先的容器网络来初始化 IB 通信即可,即在容器网络上做 ib 设备地址的发现,随后再用 ib verbs api 来通信

当然如果实现时,不是使用 tcp 的方式来初始化 IB,使用的是 rdma_cm,会有区别,容器的 ip 不能用于通信,只能用主机上的 bond0 ip 来通信 (主网卡)

有点儿奇怪可能与 rdma_cm 的 api 有关 …

另外最好加个这个,让单机多卡的训练可以 IPC 通信

1
2
3
securityContext:
capabilities:
add: [ "IPC_LOCK" ]

SR-IOV

PF -> VF

需要 SR-IOV CNI plugin 及 device plugin 配合

device plugin 挂载 /dev/infiniband 到容器中

SR-IOV CNI plugin 负责将 VF 绑定到容器的 network namespace 中

假设 VF 事先创建好

逻辑是通的,CNI 记录每个节点哪些 VF 已被分配,每个节点记录剩余几个 VF,当有新的 pod 被调度到当前节点时,CNI 即可分配未使用的 VF 与 pod

IP over Infiniband

IPoIB 由 ipoib driver 实现,能像一般 NIC (ifconfig) 一样使用 ib 网络,当然性能差一些,在到达 NIC 之前,与 socket 通信的开销一致

  • socket api
  • ib verbs api

socket api 走 os 调用栈,CPU 参与

ib verbs 直接走 ib,相当于 bypass 了 os 与 CPU,硬件卸载

不过有个地方比较疑惑,IPoIB,infiniband 底层的传输方式是 datagram 即不可靠传输,不过上层应用(zmp)都是 tcp,应有自己的重传机制;其次 ps-lite van.h 里也有开关控制重传功能,在未收到当前消息的 ACK 时,不会处理该请求,这里展开说下

van.h 初始时,如果看到打开了重传开关,则初始化 resend.h

启动了一个线程,周期性 (Timeout) 的重传 send_buf 中的消息

van.h 在

  • 发送数据时,将数据加入 send_buf 中;
  • 接收到数据时
    • 如果是 ACK,则从 send_buf 中移除该消息,并把数据交给上层处理
    • 若当前数据仍不是 ack 数据,则查看是否已发送过 ACK
      • 是的话,则认为是重复消息,不交上层处理,并发送 ACK
      • 否则 交上层处理,记录到 ACK set 中,并发送 ACK

不过这个重传的场景比较神奇,话说 TCP 已经有 ACK 和重传机制了 … 不懂 ps-lite 是遇到了什么具体的场景,再做了一次重传的增强设计

而且这个设计有个限制,收发数据越多,似乎内存消耗越大,因为记录是否收到过该消息的 ACK 的 set,并没有清理的机会,会一直增加

所以一般也未见开启这个功能,env.md 里都没说明这两环境变量,姑且认为是个废弃特性吧 …

1
2
PS_RESEND=1
PS_RESEND_TIMEOUT=1000 # ms

厂家

Azure

只支持 RDMA on IB 不支持 IP over IB

In Azure, IP over IB is not supported. Only RDMA over IB is supported.

https://docs.microsoft.com/zh-cn/azure/virtual-machines/linux/sizes-hpc#rdma-capable-instances

Aliyun

k8s 目前只看到 RDMA on IB,IP over IB 没看到,看起来是直接主机网络之后,用 ib0 ip 就行?

AWS

sagemaker 25Bbps/s 互联,看起来不像 IB or RoCE

参考

https://community.mellanox.com/s/article/kubernetes-ipoib-ethernet-rdma-sr-iov-networking-with-connectx4-connectx5

在 Kubernetes 上使用 RDMA

最近接触了一些 HPC (高性能计算) 新玩意儿,入门首先要掌握一些基本的概念

术语

MPI

Message passing interface

消息传递接口,可以理解为分布式消息传递框架

OpenMPI

MPI 的一种开源实现

RDMA

remote direct memory access

Infiniband

IB

网络设备,支持 RDMA

OpenMPI

Terminology

MCA

  • framework
  • components
  • module

An easy example framework to discuss is the MPI framework named “btl”, or the Byte Transfer Layer. It is used to send and receive data on different kinds of networks. Hence, Open MPI has btl components for shared memory, TCP, Infiniband, Myrinet, etc.

不同的 MCA 支持不同的参数,我们可以参考如下查询支持的 MCA 参数

available-mca-params

1
2
3
4
5
ompi_info --param all all --level 9
# only btl
ompi_info --param btl all --level 9
# only tcp of btl
ompi_info --param btl tcp --level 9

可以通过 MCA 参数选择 Components

selecting-components

1
mpirun --mca btl ^tcp,openib

不使用 btl framework 中的 tcp component,使用其中的 openib

mpirun

mpirun 的常用参数

  • -H: List of hosts on which to invoke processes.
  • -np: Run this many copies of the program on the given nodes. This option indicates that the specified file is an executable program and not an application context. If no value is provided for the number of copies to execute (i.e., neither the “-np” nor its synonyms are provided on the command line), Open MPI will automatically execute a copy of the program on each process slot (see below for description of a “process slot”). This feature, however, can only be used in the SPMD model and will return an error (without beginning execution of the application) otherwise.
  • –bind-to: Bind processes to the specified object, defaults to core. Supported options include slot, hwthread, core, l1cache, l2cache, l3cache, socket, numa, board, and none.
  • -x: Export the specified environment variables to the remote nodes before executing the program. Only one environment variable can be specified per -x option. Existing environment variables can be specified or new variable names specified with corresponding values. For example: % mpirun -x DISPLAY -x OFILE=/tmp/out … The parser for the -x option is not very sophisticated; it does not even understand quoted values. Users are advised to set variables in the environment, and then use -x to export (not define) them.
  • -mca: Send arguments to various MCA modules. See the “MCA” section, below.

--mca btl self 的作用

ib-btl

self 用于本地进程通信 (可能使用 lo 设备,也可能不用,例如可以使用内存共享)

openmpi,假设多机同一个地址族的地址可通,如果主机上有多个网络,openmpi 参考如下链接进行网络选择

tcp-selection

注意到 openmpi 会使用所见的所有网络,如果你不想其使用 ip network,你可以显式的禁用之,然而

Note that Open MPI will still use TCP for control messages, such as data between mpirun and the MPI processes, rendezvous information during MPI_INIT, etc. To disable TCP altogether, you also need to disable the tcp component from the OOB framework.

这句话比较有意思,一般来说 mpirun 要求 node 间可以 ssh 免密登录,而 ssh 是应用层协议,依赖 TCP

openmpi 会选择最优的网络

tcp-routability

tcp-routability-1.3

如何查看 mpirun 连接的过程

1
mpirun --mca btl self,vader,tcp --mca btl_base_verbose 30 -np 2 -host NodeA,NodeB a.out

a.out 为可执行程序

如果有 ib 卡,tcp component 会自动下线

tcp-auto-disable

openmpi build default option

default-build

1
--with-openib(=DIR) and --with-openib-libdir=DIR

  1. 计算 Replica 及 Slot 数
  2. 创建 ConfigMap
  • hostfile
1
[mpiJobName]-worker-i slots=[8]

[mpiJobName]-worker-i i.e. ${POD_NAME} of statefulset’s pod

kubexec.sh

1
2
3
4
5
#!/bin/sh
set -x
POD_NAME=$1
shift
/opt/kube/kubectl exec ${POD_NAME} -- /bin/sh -c "$*"
  1. 创建 Statefulset

Container Command: Sleep

  1. 创建 Launcher (Job)

Statefulset ready 后,创建 Launcher (Job)

设置 env OMPI_MCA_plm_rsh_agent 为 kubexec.sh

即使用 kubectl exec ${POD_NAME} -- /bin/sh -c "$*" 作为 ssh_agent

rsh-not-ssh

所以 openmpi 是有个潜在要求的,要么是支持 IPoIB,or 要有 IP 网络,纯 IB 网络不行

当然 SDP (Socket Direct Protocol) 能加速 Socket 又是另外一个话题了

1
2
3
4
5
6
7
# server side
/etc/init.d/sshd stop
env LD_PRELOAD=/usr/lib64/libsdp.so
LIBSDP_CONFIG_FILE=/u/etc/libsdp.conf /etc/init.d/sshd start
# client side
LD_PRELOAD=/usr//lib64/libsdp.so
LIBSDP_CONFIG_FILE=/etc/libsdp.conf scp <file> <user>@<IPoIBaddr>:<dir>

Running ssh, scp over SDP

lsmod | grep sdp

sdpnetstat -S

设置 env OMPI_MCA_orte_default_hostfile 为 hostfile

Container Command: mpirun

综上,mpi-operator 使用 kube-dns 获得 pod ip,mpirun 使用 kubectl exec 远程登录 container

Docker Network

IBM 的几篇 Blog overall 的讲了一下

容器如何访问外部网络

通过 docker0 网桥的外发包经过 NAT 之后 src ip 变为主机 ip

外部网络如何访问容器

容器内的端口,可在容器启动时,通过 -p 参数映射到 host 上,这时 host 上的端口会随机分配。当然也可以通过 -p [container-port]:[host-port] 方式,指定映射到 host 的特定端口

至于实现上也较为直接,若容器有 expose 端口,则 docker 会相应启动一个 docker-proxy 监听 host 上的 host 端口 (如上述例子中的 host-port),外部流量到达 host-port 时,由 docker-proxy 转发至最终容器

当然上述只是 docker network 的原生实现,docker 原生实现的不同 host 的 container 略去

Flannel

如果在 k8s 生态中,docker container 跨 host 通信,早期版本多使用 Flannel 完成

Flannel 原理

Flannel 实现的是 overlay network,即基于已有的 underlay network,在其之上扩展报文字段,完成报文转发

原理也比较好理解

  • 在 ETCD 中设置 Flannel 网段及子网范围
  • 多个 Host 上运行 Flannel daemon
  • Flannel daemon 根据 ETCD 中记录的已分配子网,确定自己的子网,并注册至 ETCD 中
  • Docker 根据 Flannel 划分的子网启动,docker0 地址从 Flannel 子网中分配得到,一般来说 Flannel0 地址为子网的第一个地址 (10.0.2.0),docker0 地址为子网的第二个地址 (10.0.2.1)

VM1 Container 1 至 VM2 Container 2 的报文转发过程

可参看该作者的一篇详细分析

看上述链接吧,讲的非常好,图文并茂,下面我只是自我温习 😆 努力积累

VM1 Container 1

  • Container 1 报文中 src ip 为容器 ip,假设为 10.1.15.2/24,dst ip 为对端容器 ip,假设为 10.1.20.3/24
  • 报文从容器中的 veth0 发往 host 上的 veth pair (veth_XXX)
  • kernel 根据 route 表将报文转发至 Flannel0 TUN
  • Flannel0 接收到之后 overlay 的作用体现了,首先根据目的 ip 查询其所在 host 的 ip,封装一层 IP 报文,随后封装一层 UDP 报文,投递到对端 Flannel daemon 监听端口 8285。这个时候报文就能通过 underlay network 转发至对端 host 了

VM2 Container 2

  • 报文到达当前 host 后,UDP 报文交由 Flannel daemon 处理
  • Flannel daemon 交由 Flannel0 TUN 处理
  • kernel 直接根据 route 表处理,转发至 docker0
  • docker0 是网桥设备,所有 docker container 均连接在其之上,因此最后根据 container dst ip 转发至 dst container

当然这是 Flannel 早期的版本,使用了 UDP 的报文封装,这样会有一些 packet 来回拷贝的开销

Flannel 还支持 VxLan 的模式,看下它的原理,网络这块还是比较有意思

这篇也很 nice An illustrated guide to Kubernetes Networking [Part 2]

nice shot An illustrated guide to Kubernetes Networking [Part 1]

这篇非常详细 … 蛤蛤

ARP 协议

ARP

Flannel VxLan

Term

ref Han’s blog

  • TUN is a software interface implemented in linux kernel, it can pass raw ip packet between user program and the kernel

Port

VIP

查询虚拟 IP,via device_owner=neutron:VIP_PORT

虚拟 IP 绑定的 IP(网卡) allowed_address_pairs

例如 VIP 192.168.186.192

1
2
3
4
5
6
7
8
9
10
allowed_address_pairs: [
{
"ip_address": "192.168.129.104",
"mac_address": "fa:16:3e:6e:e0:d8"
},
{
"ip_address": "192.168.155.84",
"mac_address": "fa:16:3e:6e:e0:d8"
}
]

VIP 可手动配置至网卡,例如给 eth0 配置 vip(ip 别名),使得 eth0 存在多个 ip

1
ifconfig eth0:1 192.168.0.107 netmask 255.255.0.0

亦或者直接添加 ip 至 dev eth0

1
ip addr add 192.168.2.105/24 dev eth0

常见做法是外部通过 VIP 访问服务,服务使用 Keeplive 组件实现 VIP 在多个后端节点漂移,从而实现服务 HA

ECS IP

查询 ECS IP(网卡),via device_id

例如 device_id=fe6b212b-9b84-4c0a-8137-528be40f0b04,即 ECS ID

主网卡有如下字段

1
2
3
{
"primary_interface": true
}

VIP 设计为绑定在其他 IP 上,因此其不存在 port_id (网卡 IP),仅存在自身的 vip_port_id,若 VIP 被多个 IP 绑定,则其对应多个 port_id

openstack 创建 ECS 过程,首先使用 neutron 命令创建 port (主网卡),其次使用 cinder 命令创建系统盘,最后使用 nova 命令根据主网卡及系统盘创建出 ECS 实例

ALL IP

查询 ALL IP,via network_id

例如 network_id=8b8457ab-521a-4da0-9cd4-aee1688ee0f8,即 VPC ID

结果包括 ECS IP、VIP 等

Up / Down

  • up 启用 network interface
  • down 停用 network interface

VPC

VPC 访问方案

VPC Endpoint

优势

  • 发布处于 VPC 中的服务,供外部使用

限制

  • 服务发布方发布服务后,需将服务标识告知使用方
  • 使用方在本 VPC 中,通过创建 VPC Endpoint 以访问服务发布方 VPC 中提供的服务,VPC Endpoint 需消耗 本 VPC 的一个 IP 资源

VPC Peering

优势

  • VPC 全互通

限制

  • 对于 VPC Peering 来说,有网段及子网的限制 ,若冲突则无法 Peering
  • VPC Peering 仅在主网卡生效,对于多网卡的主机,需额外设置路由规则,使得与 VPC Peering 通信的报文被正确转发至主网卡

当然有点儿标题党的意思

学习到这呢,已经大概有点儿感觉了

union fs

docker container 的 root fs,本质上呢都叫 ufs 技术,union file system

docker 用它来干啥的,镜像不是分层的嘛,docker 用这玩意儿技术来把所有层 union 成一个单一的 fs,给用户使用

这就是 docker container root fs 的基础了

问题就来了,现在不依赖 dockerd 咋 union file system,于是乎在 google 中搜索了下 union file system impl in golang,发现了个项目,还挺有意思

https://github.com/spf13/afero

readme 中提到它可以干

  • Support for compositional (union) file systems by combining multiple file systems acting as one

看看能不能用吧

显然不能 … 粗略一扫,就是一些 os api 封装,文档也不友好 sigh

still docker pull

docker pull 的大概过程,pull 镜像,随后使用 graph driver union mount,最后把 image 注册到 layer store

怎么看的,在 daemon/graphdriver/aufs 往上搜就行,最后发现 docker pull 也用了它

所以回答上篇的问题 扫描镜像时,为何不把 layer union 之后,再扫描,看到这,诸位可能已经发现不好实现呀

能不能实现,当然能!

  1. 按照这里所说 loading-an-image-filesystem-changeset
    1.1 untar root layer to a dir (act as container root fs)
    1.2 untar each layer to the previous dir, and walk once, rm any file with .wh. prefix and its coresponding file
    1.3 continue this process
    1.4 … pay attention, 可能有童鞋会觉得这个细节可能因 storage driver 而异,实则不然,image tar archive 的格式是独立于 storage driver 的
  2. 熟悉 docker layer 代码的老铁,没准能把这部分代码给整出个独立的 lib 来,实现把 image layer union mount 之后,给扫描程序一个统一的 fs view, 但是显然它依赖于 storage driver 的能力,你要想在容器里面干这个事情,我就 🙄 了。要是非得在容器里这么折腾,不如直接挂 docker socket 到容器里,用宿主机的 dockerd 直接搞来的快些,废这大劲儿 sucks

https://docs.docker.com/storage/storagedriver/
Storage drivers allow you to create data in the writable layer of your container. The files won’t be persisted after the container is deleted, and both read and write speeds are low.

也是够精辟

不过我还是有个疑问,不同 storage driver 实现分层镜像的细节不同,docker save 的时候,是怎么把不同 storage driver 的 layer 能统一到 Image Tar File Archive 里面去的

手头上没有试验 devicemapper 的机器,按说 divicemapper 实现分层镜像用的是 snapshot 技术,所以删除文件的时候,当前 layer 并不会有 .wh. 文件才对

这么说来,似乎是 layer diff 是 docker 自己算出来的了,删除的文件,给标记上 .wh. ?

whatever it needs time to cover it

https://learn-docker-the-hard-way.readthedocs.io/zh_CN/latest/

最后的时候,发现 google 又为世界造轮子了

https://github.com/GoogleContainerTools/container-diff

行吧,google 大佬已经做了,而且的确有 lib,效果好不好那就再说了,这个库基本上实现了 fundamental 的 loading-an-image-filesystem-changeset 描述的过程

当然因为是 file diff,所以权限恢复不出来的