diff --git a/doc_cn/build_and_install/distributed_training_on_kubernetes.md b/doc_cn/build_and_install/distributed_training_on_kubernetes.md deleted file mode 100644 index d1769bb40aabfb0ad4e3ec3c0e9c6024efba0fc2..0000000000000000000000000000000000000000 --- a/doc_cn/build_and_install/distributed_training_on_kubernetes.md +++ /dev/null @@ -1,289 +0,0 @@ - -# Paddle on Kubernetes:分布式训练 - -前一篇文章介绍了如何在Kubernetes集群上启动一个单机Paddle训练作业 (Job)。在这篇文章里,我们介绍如何在Kubernetes集群上启动分布式Paddle训练作业。关于Paddle的分布式集群训练,可以参考 [Cluster Training](https://github.com/baidu/Paddle/blob/develop/doc/cluster/opensource/cluster_train.md), 本文在此基础上,利用Kubernetes快速构建Paddle集群,进行分布式训练任务。 - -## 制作镜像 - -Paddle的集群训练需要有一个Paddle集群来实现,在本文中,我们使用Kubernetes来快速创建一个Paddle集群。我们使用 `paddledev/paddle:cpu-demo-latest` 镜像作为Paddle集群节点的运行环境,里面包含了 Paddle 运行所需要的相关依赖,同时,为了能将训练任务及配置统一分发到各个节点,需要使用到`sshd`以便使用`fabric`来操作。镜像的 Dockerfile 如下: - -``` -FROM paddledev/paddle:cpu-demo-latest - -RUN apt-get update -RUN apt-get install -y openssh-server -RUN mkdir /var/run/sshd -RUN echo 'root:root' | chpasswd - -RUN sed -ri 's/^PermitRootLogin\s+.*/PermitRootLogin yes/' /etc/ssh/sshd_config -RUN sed -ri 's/UsePAM yes/#UsePAM yes/g' /etc/ssh/sshd_config - -EXPOSE 22 - -CMD ["/usr/sbin/sshd", "-D"] -``` - -使用 `docker build` 构建镜像: - -``` -docker build -t mypaddle:paddle_demo_ssh . -``` - -## 准备工作空间 - -工作空间 [Job Workspace](https://github.com/baidu/Paddle/blob/develop/doc/cluster/opensource/cluster_train.md#prepare-job-workspace) , 即一个包含了依赖库,训练,测试数据,模型配置文件的目录。参考 [Cluster Training](https://github.com/baidu/Paddle/blob/develop/doc/cluster/opensource/cluster_train.md)中的例子,我们也是用`demo/recommendation`作为本文的训练任务。此demo可直接从[Github Paddle源码](https://github.com/baidu/Paddle/tree/develop/demo/recommendation)中获取。 - -### 准备训练数据 - -在Paddle源码中,找到`demo/recommendation`文件夹,即为我们的Workspace, 在本文的环境中,路径为`/home/work/paddle-demo/Paddle/demo/recommendation` - -``` -[root@paddle-k8s-node0 recommendation]# tree -. -├── common_utils.py -├── data -│   ├── config_generator.py -│   ├── config.json -│   ├── meta_config.json -│   ├── meta_generator.py -│   ├── ml_data.sh -│   └── split.py -├── dataprovider.py -├── evaluate.sh -├── prediction.py -├── preprocess.sh -├── requirements.txt -├── run.sh -└── trainer_config.py - -1 directory, 14 files -``` - -运行`data/ml_data.sh`脚本,下载数据,然后运行`preprocess.sh`脚本进行预处理。 - -``` -[root@paddle-k8s-node0 recommendation]# data/ml_data.sh -++ dirname data/ml_data.sh -+ cd data -+ wget http://files.grouplens.org/datasets/movielens/ml-1m.zip ---2016-11-04 10:14:49-- http://files.grouplens.org/datasets/movielens/ml-1m.zip -Resolving files.grouplens.org (files.grouplens.org)... 128.101.34.146 -Connecting to files.grouplens.org (files.grouplens.org)|128.101.34.146|:80... connected. -HTTP request sent, awaiting response... 200 OK -Length: 5917549 (5.6M) [application/zip] -Saving to: ‘ml-1m.zip’ - -100%[==========================>] 5,917,549 50.6KB/s in 2m 29s - -2016-11-04 10:17:20 (38.8 KB/s) - ‘ml-1m.zip’ saved [5917549/5917549] - -+ unzip ml-1m.zip -Archive: ml-1m.zip - creating: ml-1m/ - inflating: ml-1m/movies.dat - inflating: ml-1m/ratings.dat - inflating: ml-1m/README - inflating: ml-1m/users.dat -+ rm ml-1m.zip - -[root@paddle-k8s-node0 recommendation]# ./preprocess.sh -generate meta config file -generate meta file -split train/test file -shuffle train file -``` - -### 修改集群训练配置 - -参考[Cluster Training](https://github.com/baidu/Paddle/blob/develop/doc/cluster/opensource/cluster_train.md)中的介绍,我们使用`paddle/scripts/cluster_train/`中的文件来作为分布式训练任务的配置和启动脚本。在`run.sh`文件中,填入我们的workspace和训练配置文件路径。 - -``` -#!/bin/sh -python paddle.py \ - --job_dispatch_package="/home/work/paddle-demo/Paddle/demo/recommendation" \ - --dot_period=10 \ - --ports_num_for_sparse=2 \ - --log_period=50 \ - --num_passes=10 \ - --trainer_count=4 \ - --saving_period=1 \ - --local=0 \ - --config=/home/work/paddle-demo/Paddle/demo/recommendation/trainer_config.py \ - --save_dir=./output \ - --use_gpu=0 -``` - -## 创建Paddle集群 - -创建Paddle集训需要编写创建Kubernetes资源的yaml文件,首先,创建一个Service,便于我们通过此Service来查找其对应的Paddle节点。 - -``` -apiVersion: v1 -kind: Service -metadata: - name: cluster-demo -spec: - selector: - app: cluster-demo - ports: - - name: default - protocol: TCP - port: 7164 - targetPort: 7164 -``` - -为了创建多个Paddle节点,我们使用Kubernetes ReplicationController资源来控制Paddle集群中的节点数量,Paddle节点之间需要开放相关的端口来互相通信。下面的例子中,我们开放了每个Paddle节点的7164-7167端口,例如,一个包含4个节点的Paddle集群的yaml文件如下: - -``` -apiVersion: v1 -kind: ReplicationController -metadata: - name: cluster-demo -spec: - replicas: 4 - selector: - app: cluster-demo - template: - metadata: - name: cluster-demo - labels: - app: cluster-demo - spec: - containers: - - name: cluster-demo - image: mypaddle:paddle_demo_ssh - ports: - - containerPort: 7164 - - containerPort: 7165 - - containerPort: 7166 - - containerPort: 7167 -``` - -然后我们可以通过`kubectl`工具来查看所创建的资源信息。 - -首先查看我们创建的Paddle Service,然后根据Service,查看所创建的Paddle节点的IP地址。 - -``` -[root@paddle-k8s-node0 cluster_train]# kubectl get svc -NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE -cluster-demo 11.1.1.77 7164/TCP 6h - -[root@paddle-k8s-node0 cluster_train]# kubectl get -o json endpoints cluster-demo | grep ip - "ip": "192.168.129.79", - "ip": "192.168.129.80", - "ip": "192.168.223.157", - "ip": "192.168.223.158", -``` - -## 开始集群训练 - -我们需要在`paddle/scripts/cluster_train/conf.py`文件中指定各个节点的IP地址以及开放的端口。根据上文创建的信息,`conf.py`文件修改如下: - -``` -HOSTS = [ - "root@192.168.129.79", - "root@192.168.129.80", - "root@192.168.223.157", - "root@192.168.223.158" - ] - -''' -workspace configuration -''' -#root dir for workspace, can be set as any director with real user account -ROOT_DIR = "/home/paddle" - - -''' -network configuration -''' -#pserver nics -PADDLE_NIC = "eth0" -#pserver port -PADDLE_PORT = 7164 -#pserver ports num -PADDLE_PORTS_NUM = 2 -#pserver sparse ports num -PADDLE_PORTS_NUM_FOR_SPARSE = 2 - -#environments setting for all processes in cluster job -LD_LIBRARY_PATH="/usr/local/cuda/lib64:/usr/lib64" -``` -然后使用`run.sh`脚本开始训练,启动的打印如下: - -``` -[root@paddle-k8s-node0 cluster_train]# ./run.sh -[root@192.168.129.79] Executing task 'job_create_workspace' -...... -[root@192.168.129.80] Executing task 'job_create_workspace' -...... -[root@192.168.223.157] Executing task 'job_create_workspace' -...... -[root@192.168.223.158] Executing task 'job_create_workspace' -...... -[root@192.168.129.79] run: echo 0 > /home/paddle/JOB20161104171630/nodefile -[root@192.168.129.80] Executing task 'set_nodefile' -[root@192.168.129.80] run: echo 1 > /home/paddle/JOB20161104171630/nodefile -[root@192.168.223.157] Executing task 'set_nodefile' -[root@192.168.223.157] run: echo 2 > /home/paddle/JOB20161104171630/nodefile -[root@192.168.223.158] Executing task 'set_nodefile' -[root@192.168.223.158] run: echo 3 > /home/paddle/JOB20161104171630/nodefile -``` - -可以看到192.168.129.79,192.168.129.80,192.168.223.157,192.168.223.158分别为Paddle集群的Node 0-3. - -我们可以进入其中一个Paddle节点查看训练的日志。 - -``` -root@cluster-demo-fwwi5:/home/paddle/JOB20161104171700/log# less paddle_trainer.INFO -Log file created at: 2016/11/04 09:17:20 -Running on machine: cluster-demo-fwwi5 -Log line format: [IWEF]mmdd hh:mm:ss.uuuuuu threadid file:line] msg -I1104 09:17:20.346797 108 Util.cpp:155] commandline: /usr/local/bin/../opt/paddle/bin/paddle -_trainer --num_gradient_servers=4 --nics=eth0 --port=7164 --ports_num=2 --comment=paddle_proce -ss_by_paddle --pservers=192.168.129.79,192.168.129.80,192.168.223.157,192.168.223.158 --ports_ -num_for_sparse=2 --config=./trainer_config.py --trainer_count=4 --use_gpu=0 --num_passes=10 -- -save_dir=./output --log_period=50 --dot_period=10 --saving_period=1 --local=0 --trainer_id=1 - -root@cluster-demo-fwwi5:/home/paddle/JOB20161104171700/log# tailf paddle_trainer.INFO -...... -I1104 09:17:37.376471 150 ThreadLocal.cpp:37] thread use undeterministic rand seed:151 -I1104 09:18:54.159624 108 TrainerInternal.cpp:163] Batch=50 samples=80000 AvgCost=4.03478 CurrentCost=4.03478 Eval: CurrentEval: - -I1104 09:20:10.207902 108 TrainerInternal.cpp:163] Batch=100 samples=160000 AvgCost=3.75806 CurrentCost=3.48134 Eval: CurrentEval: -I1104 09:21:26.493571 108 TrainerInternal.cpp:163] Batch=150 samples=240000 AvgCost=3.64512 CurrentCost=3.41923 Eval: CurrentEval: - -``` - -最后,我们可以在Paddle集群的node0(192.168.129.79)上查看训练的输出结果。 - -``` -[root@paddle-k8s-node0 ~]# ssh root@192.168.129.79 -...... -root@cluster-demo-r65g0:/home/paddle/JOB20161104171700/output/pass-00000# ll -total 14876 -drwxr-xr-x. 2 root root 4096 Nov 4 09:40 ./ -drwxr-xr-x. 3 root root 23 Nov 4 09:40 ../ --rw-r--r--. 1 root root 4046864 Nov 4 09:40 ___embedding_0__.w0 --rw-r--r--. 1 root root 100368 Nov 4 09:40 ___embedding_1__.w0 --rw-r--r--. 1 root root 6184976 Nov 4 09:40 ___embedding_2__.w0 --rw-r--r--. 1 root root 2064 Nov 4 09:40 ___embedding_3__.w0 --rw-r--r--. 1 root root 7184 Nov 4 09:40 ___embedding_4__.w0 --rw-r--r--. 1 root root 21520 Nov 4 09:40 ___embedding_5__.w0 --rw-r--r--. 1 root root 262160 Nov 4 09:40 ___fc_layer_0__.w0 --rw-r--r--. 1 root root 1040 Nov 4 09:40 ___fc_layer_0__.wbias -...... -...... --rw-r--r--. 1 root root 262160 Nov 4 09:40 _movie_fusion.w0 --rw-r--r--. 1 root root 262160 Nov 4 09:40 _movie_fusion.w1 --rw-r--r--. 1 root root 262160 Nov 4 09:40 _movie_fusion.w2 --rw-r--r--. 1 root root 1040 Nov 4 09:40 _movie_fusion.wbias --rw-r--r--. 1 root root 262160 Nov 4 09:40 _user_fusion.w0 --rw-r--r--. 1 root root 262160 Nov 4 09:40 _user_fusion.w1 --rw-r--r--. 1 root root 262160 Nov 4 09:40 _user_fusion.w2 --rw-r--r--. 1 root root 262160 Nov 4 09:40 _user_fusion.w3 --rw-r--r--. 1 root root 1040 Nov 4 09:40 _user_fusion.wbias --rw-r--r--. 1 root root 169 Nov 4 09:40 done --rw-r--r--. 1 root root 17 Nov 4 09:40 path.txt --rw-r--r--. 1 root root 3495 Nov 4 09:40 trainer_config.py -``` \ No newline at end of file diff --git a/doc_cn/cluster/k8s/Dockerfile b/doc_cn/cluster/k8s/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..3a73606c61432329b4cc2d2f8daadc5af8735c96 --- /dev/null +++ b/doc_cn/cluster/k8s/Dockerfile @@ -0,0 +1,7 @@ +FROM paddledev/paddle:cpu-latest + +MAINTAINER zjsxzong89@gmail.com + +COPY start.sh /root/ +COPY start_paddle.py /root/ +CMD ["bash"," -c","/root/start.sh"] \ No newline at end of file diff --git a/doc_cn/cluster/k8s/distributed_training_on_kubernetes.md b/doc_cn/cluster/k8s/distributed_training_on_kubernetes.md new file mode 100644 index 0000000000000000000000000000000000000000..8e947f8d56a4e67287e7a3b8692a6b7fe4ce194e --- /dev/null +++ b/doc_cn/cluster/k8s/distributed_training_on_kubernetes.md @@ -0,0 +1,254 @@ + +# Paddle on Kubernetes:分布式训练 + +前一篇文章介绍了如何在Kubernetes集群上启动一个单机Paddle训练作业 (Job)。在这篇文章里,我们介绍如何在Kubernetes集群上进行分布式Paddle训练作业。关于Paddle的分布式训练,可以参考 [Cluster Training](https://github.com/baidu/Paddle/blob/develop/doc/cluster/opensource/cluster_train.md), 本文利用Kubernetes的调度功能与容器编排能力,快速构建Paddle容器集群,进行分布式训练任务。 + +## Kubernetes 基本概念 + +在介绍分布式训练之前,需要对Kubernetes(k8s)有一个基本的认识,下面先简要介绍一下本文用到的几个k8s概念。 + +### Node + +[`Node`](http://kubernetes.io/docs/admin/node/) 表示一个k8s集群中的一个工作节点,这个节点可以是物理机或者虚拟机,k8s集群就是由`node`节点与`master`节点组成的。每个node都安装有Docker,在本文的例子中,`Paadle`容器就在node上运行。 + +### Pod + +一个[`Pod`](http://kubernetes.io/docs/user-guide/pods/) 是一组(一个或多个)容器,pod是k8s的最小调度单元,一个pod中的所有容器会被调度到同一个node上。Pod中的容器共享NET,PID,IPC,UTS等Linux namespace,它们使用同一个IP地址,可以通过`localhost`互相通信。不同pod之间可以通过IP地址访问。 + +### Job + +[`Job`](http://kubernetes.io/docs/user-guide/jobs/) 可以翻译为作业,每个job可以设定pod成功完成的次数,一次作业会创建多个pod,当成功完成的pod个数达到预设值时,就表示job成功结束了。 + +### Volume + +[`Volume`](http://kubernetes.io/docs/user-guide/volumes/) 存储卷,是pod内的容器都可以访问的共享目录,也是容器与node之间共享文件的方式,因为容器内的文件都是暂时存在的,当容器因为各种原因被销毁时,其内部的文件也会随之消失。通过volume,就可以将这些文件持久化存储。k8s支持多种volume,例如`hostPath(宿主机目录)`,`gcePersistentDisk`,`awsElasticBlockStore`等。 + +### Namespace + +[`Namespaces`](http://kubernetes.io/docs/user-guide/volumes/) 命名空间,在k8s中创建的所有资源对象(例如上文的pod,job)等都属于一个命名空间,在同一个命名空间中,资源对象的名字是唯一的,不同空间的资源名可以重复,命名空间主要用来为不同的用户提供相对隔离的环境。本文只使用了`default`默认命名空间,读者可以不关心此概念。 + +## 整体方案 + +### 前提条件 + +首先,我们需要拥有一个k8s集群,在这个集群中所有node与pod都可以互相通信。关于k8s集群搭建,可以参考[官方文档](http://kubernetes.io/docs/getting-started-guides/kubeadm/),在以后的文章中我们也会介绍AWS上搭建的方案。在本文的环境中,k8s集群中所有node都挂载了一个`mfs`(分布式文件系统)共享目录,我们通过这个目录来存放训练文件与最终输出的模型。在训练之前,用户将配置与训练数据切分好放在mfs目录中,训练时,程序从此目录拷贝文件到容器内进行训练,将结果保存到此目录里。 + +### 使用 `Job` + +我们使用k8s中的job这个概念来代表一次分布式训练。`Job`表示一次性作业,在作业完成后,k8s会销毁job产生的容器并且释放相关资源。 + +在k8s中,可以通过编写一个 `yaml` 文件,来描述这个job,在这个文件中,主要包含了一些配置信息,例如Paddle节点的个数,`paddle pserver`开放的端口个数与端口号,`paddle`使用的网卡设备等,这些信息通过环境变量的形式传递给容器内的程序使用。 + +在一次分布式训练中,用户确定好本次训练需要的Paddle节点个数,将切分好的训练数据与配置文件上传到`mfs`共享目录中。然后编写这次训练的`job yaml`文件,提交给k8s集群创建并开始作业。 + +### 创建`Paddle`节点 + +当k8s master收到`job yaml`文件后,会解析相关字段,创建出多个pod(个数为Paddle节点数),k8s会把这些pod调度到集群的node上运行。一个`pod`就代表一个`Paddle`节点,当pod被成功分配到一台物理/虚拟机上后,k8s会启动pod内的容器,这个容器会根据`job yaml`文件中的环境变量,启动`paddle pserver`与`paddle train`进程。 + +### 启动训练 + +在容器启动后,会通过脚本来启动这次分布式训练,我们知道`paddle train`进程启动时需要知道其他节点的IP地址以及本节点的`trainer_id`,由于`Paddle`本身不提供类似服务发现的功能,所以在本文的启动脚本中,每个节点会根据`job name`向`k8s apiserver`查询这个`job`对应的所有`pod`信息(k8s默认会在每个容器的环境变量中写入`apiserver`的地址)。 + +根据这些pod信息,就可以通过某种方式,为每个pod分配一个唯一的`trainer_id`。本文把所有pod的IP地址进行排序,将顺序作为每个`Paddle`节点的`trainer_id`。启动脚本的工作流程大致如下: + + 1. 查询`k8s apiserver`获取pod信息,根据IP分配`trainer_id` + 1. 从`mfs`共享目录中拷贝训练文件到容器内 + 1. 根据环境变量,解析出`paddle pserver`与`paddle train`的启动参数,启动进程 + 1. 训练时,`Paddle`会自动将结果保存在`trainer_id`为0的节点上,将输出路径设置为`mfs`目录,保存输出的文件 + + +## 搭建过程 + +根据前文的描述,要在已有的k8s集群上进行`Paddle`的分布式训练,主要分为以下几个步骤: + +1. 制作`Paddle`镜像 +1. 将训练文件与切分好的数据上传到共享存储 +1. 编写本次训练的`job yaml`文件,创建`k8s job` +1. 训练结束后查看输出结果 + +下面就根据这几个步骤分别介绍。 + + + +### 制作镜像 + +`Paddle`镜像需要提供`paddle pserver`与`paddle train`进程的运行环境,用这个镜像创建的容器需要有以下两个功能: + +- 拷贝训练文件到容器内 + +- 生成`paddle pserver`与`paddle train`进程的启动参数,并且启动训练 + +因为官方镜像 `paddledev/paddle:cpu-latest` 内已经包含`Paddle`的执行程序但是还没上述功能,所以我们可以在这个基础上,添加启动脚本,制作新镜像来完成以上的工作。镜像的`Dockerfile`如下: + +```Dockerfile +FROM paddledev/paddle:cpu-latest + +MAINTAINER zjsxzong89@gmail.com + +COPY start.sh /root/ +COPY start_paddle.py /root/ +CMD ["bash"," -c","/root/start.sh"] +``` + +[`start.sh`](start.sh)文件拷贝训练文件到容器内,然后执行[`start_paddle.py`](start_paddle.py)脚本启动训练,前文提到的获取其他节点IP地址,分配`trainer_id`等都在`start_paddle.py`脚本中完成。 + + +使用 `docker build` 构建镜像: + +```bash +docker build -t registry.baidu.com/public/paddle:mypaddle . +``` + +然后将构建成功的镜像上传到镜像仓库,注意本文中使用的`registry.baidu.com`是一个私有仓库,读者可以根据自己的情况部署私有仓库或者使用`Docker hub`。 + +```bash +docker push registry.baidu.com/public/paddle:mypaddle +``` + +### 上传训练文件 + +本文使用`Paddle`官方的`recommendation demo`作为这次训练的内容,我们将训练文件与数据放在一个`job name`命名的目录中,上传到`mfs`共享存储。完成后`mfs`上的文件内容大致如下: + +```bash +[root@paddle-k8s-node0 mfs]# tree -d +. +└── paddle-cluster-job + ├── data + │   ├── 0 + │   │ + │   ├── 1 + │   │ + │   └── 2 + ├── output + └── recommendation +``` + +目录中`paddle-cluster-job`是本次训练对应的`job name`,本次训练要求有3个`Paddle`节点,在`paddle-cluster-job/data`目录中存放切分好的数据,文件夹`0,1,2`分别代表3个节点的`trainer_id`。`recommendation`文件夹内存放训练文件,`output`文件夹存放训练结果与日志。 + +### 创建`job` + +`k8s`可以通过`yaml`文件来创建相关对象,然后可以使用命令行工具创建`job`。 + +`job yaml`文件描述了这次训练使用的Docker镜像,需要启动的节点个数以及 `paddle pserver`与 `paddle train`进程启动的必要参数,也描述了容器需要使用的存储卷挂载的情况。`yaml`文件中各个字段的具体含义,可以查看[`k8s官方文档`](http://kubernetes.io/docs/api-reference/batch/v1/definitions/#_v1_job)。例如,本次训练的`yaml`文件可以写成: + +```yaml +apiVersion: batch/v1 +kind: Job +metadata: + name: paddle-cluster-job +spec: + parallelism: 3 + completions: 3 + template: + metadata: + name: paddle-cluster-job + spec: + volumes: + - name: jobpath + hostPath: + path: /home/work/mfs + containers: + - name: trainer + image: registry.baidu.com/public/paddle:mypaddle + command: ["bin/bash", "-c", "/root/start.sh"] + env: + - name: JOB_NAME + value: paddle-cluster-job + - name: JOB_PATH + value: /home/jobpath + - name: JOB_NAMESPACE + value: default + - name: TRAIN_CONFIG_DIR + value: recommendation + - name: CONF_PADDLE_NIC + value: eth0 + - name: CONF_PADDLE_PORT + value: "7164" + - name: CONF_PADDLE_PORTS_NUM + value: "2" + - name: CONF_PADDLE_PORTS_NUM_SPARSE + value: "2" + - name: CONF_PADDLE_GRADIENT_NUM + value: "3" + volumeMounts: + - name: jobpath + mountPath: /home/jobpath + restartPolicy: Never +``` + +文件中,`metadata`下的`name`表示这个`job`的名字。`parallelism,completions`字段表示这个`job`会同时开启3个`Paddle`节点,成功训练且退出的`pod`数目为3时,这个`job`才算成功结束。然后申明一个存储卷`jobpath`,代表宿主机目录`/home/work/mfs`,在对容器的描述`containers`字段中,将此目录挂载为容器的`/home/jobpath`目录,这样容器的`/home/jobpath`目录就成为了共享存储,放在这个目录里的文件其实是保存到了`mfs`上。 + +`env`字段表示容器的环境变量,我们将`paddle`运行的一些参数通过这种方式传递到容器内。 + +`JOB_PATH`表示共享存储挂载的路径,`JOB_NAME`表示job名字,`TRAIN_CONFIG_DIR`表示本次训练文件所在目录,这三个变量组合就可以找到本次训练需要的文件路径。 + +`CONF_PADDLE_NIC`表示`paddle pserver`进程需要的`--nics`参数,即网卡名 + +`CONF_PADDLE_PORT`表示`paddle pserver`的`--port`参数,`CONF_PADDLE_PORTS_NUM`则表示稠密更新的端口数量,也就是`--ports_num`参数。 + +`CONF_PADDLE_PORTS_NUM_SPARSE`表示稀疏更新的端口数量,也就是`--ports_num_for_sparse`参数。 + +`CONF_PADDLE_GRADIENT_NUM`表示训练节点数量,即`--num_gradient_servers`参数 + +编写完`yaml`文件后,可以使用k8s的命令行工具创建`job`. + +```bash +kubectl create -f job.yaml +``` + +创建成功后,k8s就会创建3个`pod`作为`Paddle`节点然后拉取镜像,启动容器开始训练。 + + +### 查看输出 + +在训练过程中,可以在共享存储上查看输出的日志和模型,例如`output`目录下就存放了输出结果。注意`node_0`,`node_1`,`node_2`这几个目录表示`Paddle`节点与`trainer_id`,并不是k8s中的`node`概念。 + +```bash +[root@paddle-k8s-node0 output]# tree -d +. +├── node_0 +│   ├── server.log +│   └── train.log +├── node_1 +│   ├── server.log +│   └── train.log +├── node_2 +...... +├── pass-00002 +│   ├── done +│   ├── ___embedding_0__.w0 +│   ├── ___embedding_1__.w0 +...... +``` + +我们可以通过日志查看容器训练的情况,例如: + +```bash +[root@paddle-k8s-node0 node_0]# cat train.log +I1116 09:10:17.123121 50 Util.cpp:155] commandline: + /usr/local/bin/../opt/paddle/bin/paddle_trainer + --nics=eth0 --port=7164 + --ports_num=2 --comment=paddle_process_by_paddle + --pservers=192.168.129.66,192.168.223.143,192.168.129.71 + --ports_num_for_sparse=2 --config=./trainer_config.py + --trainer_count=4 --num_passes=10 --use_gpu=0 + --log_period=50 --dot_period=10 --saving_period=1 + --local=0 --trainer_id=0 + --save_dir=/home/jobpath/paddle-cluster-job/output +I1116 09:10:17.123440 50 Util.cpp:130] Calling runInitFunctions +I1116 09:10:17.123764 50 Util.cpp:143] Call runInitFunctions done. +[WARNING 2016-11-16 09:10:17,227 default_decorators.py:40] please use keyword arguments in paddle config. +[INFO 2016-11-16 09:10:17,239 networks.py:1282] The input order is [movie_id, title, genres, user_id, gender, age, occupation, rating] +[INFO 2016-11-16 09:10:17,239 networks.py:1289] The output order is [__regression_cost_0__] +I1116 09:10:17.392917 50 Trainer.cpp:170] trainer mode: Normal +I1116 09:10:17.613910 50 PyDataProvider2.cpp:257] loading dataprovider dataprovider::process +I1116 09:10:17.680917 50 PyDataProvider2.cpp:257] loading dataprovider dataprovider::process +I1116 09:10:17.681543 50 GradientMachine.cpp:134] Initing parameters.. +I1116 09:10:18.012390 50 GradientMachine.cpp:141] Init parameters done. +I1116 09:10:18.018641 50 ParameterClient2.cpp:122] pserver 0 192.168.129.66:7164 +I1116 09:10:18.018950 50 ParameterClient2.cpp:122] pserver 1 192.168.129.66:7165 +I1116 09:10:18.019069 50 ParameterClient2.cpp:122] pserver 2 192.168.223.143:7164 +I1116 09:10:18.019492 50 ParameterClient2.cpp:122] pserver 3 192.168.223.143:7165 +I1116 09:10:18.019716 50 ParameterClient2.cpp:122] pserver 4 192.168.129.71:7164 +I1116 09:10:18.019836 50 ParameterClient2.cpp:122] pserver 5 192.168.129.71:7165 +``` \ No newline at end of file diff --git a/doc_cn/cluster/k8s/job.yaml b/doc_cn/cluster/k8s/job.yaml new file mode 100644 index 0000000000000000000000000000000000000000..1e0ac464b2ec71e98c28f090124690b01b0755ce --- /dev/null +++ b/doc_cn/cluster/k8s/job.yaml @@ -0,0 +1,43 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: paddle-cluster-job +spec: + parallelism: 3 + completions: 3 + template: + metadata: + name: paddle-cluster-job + spec: + volumes: + - name: jobpath + hostPath: + path: /home/work/paddle_output + containers: + - name: trainer + image: registry.baidu.com/public/paddle:mypaddle + command: ["bin/bash", "-c", "/root/start.sh"] + env: + - name: JOB_NAME + value: paddle-cluster-job + - name: JOB_PATH + value: /home/jobpath + - name: JOB_NAMESPACE + value: default + - name: TRAIN_CONFIG_DIR + value: recommendation + - name: CONF_PADDLE_NIC + value: eth0 + - name: CONF_PADDLE_PORT + value: "7164" + - name: CONF_PADDLE_PORTS_NUM + value: "2" + - name: CONF_PADDLE_PORTS_NUM_SPARSE + value: "2" + - name: CONF_PADDLE_GRADIENT_NUM + value: "3" + volumeMounts: + - name: jobpath + mountPath: /home/jobpath + restartPolicy: Never + \ No newline at end of file diff --git a/doc_cn/cluster/k8s/start.sh b/doc_cn/cluster/k8s/start.sh new file mode 100755 index 0000000000000000000000000000000000000000..b3a1334174a20b018d35de3b01b149fc5b10d49d --- /dev/null +++ b/doc_cn/cluster/k8s/start.sh @@ -0,0 +1,19 @@ +#!/bin/sh +set -eu + +jobconfig=${JOB_PATH}"/"${JOB_NAME}"/"${TRAIN_CONFIG_DIR} +cd /root +cp -rf $jobconfig . +cd $TRAIN_CONFIG_DIR + + +python /root/start_paddle.py \ + --dot_period=10 \ + --ports_num_for_sparse=$CONF_PADDLE_PORTS_NUM \ + --log_period=50 \ + --num_passes=10 \ + --trainer_count=4 \ + --saving_period=1 \ + --local=0 \ + --config=./trainer_config.py \ + --use_gpu=0 diff --git a/doc_cn/cluster/k8s/start_paddle.py b/doc_cn/cluster/k8s/start_paddle.py new file mode 100755 index 0000000000000000000000000000000000000000..bc0112a77fb84db8965a09716006377c127ad4db --- /dev/null +++ b/doc_cn/cluster/k8s/start_paddle.py @@ -0,0 +1,159 @@ +#!/usr/bin/python +# Copyright (c) 2016 Baidu, Inc. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import requests +import time +import socket +import os +import argparse + + +# configuration for cluster +API = "/api/v1/namespaces/" +JOBSELECTOR = "labelSelector=job-name=" +JOB_PATH = os.getenv("JOB_PATH") + "/" + os.getenv("JOB_NAME") +JOB_PATH_DATA = JOB_PATH + "/data" +JOB_PATH_OUTPUT = JOB_PATH + "/output" +JOBNAME = os.getenv("JOB_NAME") +NAMESPACE = os.getenv("JOB_NAMESPACE") +PADDLE_NIC = os.getenv("CONF_PADDLE_NIC") +PADDLE_PORT = os.getenv("CONF_PADDLE_PORT") +PADDLE_PORTS_NUM = os.getenv("CONF_PADDLE_PORTS_NUM") +PADDLE_PORTS_NUM_SPARSE = os.getenv("CONF_PADDLE_PORTS_NUM_SPARSE") +PADDLE_SERVER_NUM = os.getenv("CONF_PADDLE_GRADIENT_NUM") + + +def refine_unknown_args(cmd_args): + ''' + refine unknown parameters to handle some special parameters + ''' + new_args = [] + for arg in cmd_args: + if arg.startswith("--") and arg.find("=") != -1: + equal_pos = arg.find("=") # find first = pos + arglist = list(arg) + arglist[equal_pos] = " " + arg = "".join(arglist) + arg = arg.lstrip("-") + new_args += arg.split(" ") + elif arg.startswith("--") and arg.find("=") == -1: + arg = arg.lstrip("-") + new_args.append(arg) + else: + new_args.append(arg) + return new_args + + +def isPodAllRunning(podlist): + ''' + check all pod is running + ''' + require = len(podlist["items"]) + running = 0 + for pod in podlist["items"]: + if pod["status"]["phase"] == "Running": + running += 1 + if require == running: + return True + return False + + +def getPodList(): + ''' + get all container status of the job + ''' + apiserver = "https://" + \ + os.getenv("KUBERNETES_SERVICE_HOST") + ":" + \ + os.getenv("KUBERNETES_SERVICE_PORT_HTTPS") + + pod = API + NAMESPACE + "/pods?" + job = JOBNAME + return requests.get(apiserver + pod + JOBSELECTOR + job, + verify=False).json() + + +def getIdMap(podlist): + ''' + generate tainer_id by ip + ''' + ips = [] + for pod in podlist["items"]: + ips.append(pod["status"]["podIP"]) + ips.sort() + idMap = {} + for i in range(len(ips)): + idMap[ips[i]] = i + return idMap + + +def startPaddle(idMap={}, train_args_dict=None): + ''' + start paddle pserver and trainer + ''' + program = 'paddle train' + args = " --nics=" + PADDLE_NIC + args += " --port=" + str(PADDLE_PORT) + args += " --ports_num=" + str(PADDLE_PORTS_NUM) + args += " --comment=" + "paddle_process_by_paddle" + ip_string = "" + for ip in idMap.keys(): + ip_string += (ip + ",") + ip_string = ip_string.rstrip(",") + args += " --pservers=" + ip_string + args_ext = "" + for key, value in train_args_dict.items(): + args_ext += (' --' + key + '=' + value) + localIP = socket.gethostbyname(socket.gethostname()) + trainerId = idMap[localIP] + args += " " + args_ext + " --trainer_id=" + \ + str(trainerId) + " --save_dir=" + JOB_PATH_OUTPUT + logDir = JOB_PATH_OUTPUT + "/node_" + str(trainerId) + if not os.path.exists(JOB_PATH_OUTPUT): + os.makedirs(JOB_PATH_OUTPUT) + os.mkdir(logDir) + copyCommand = 'cp -rf ' + JOB_PATH_DATA + \ + "/" + str(trainerId) + " ./data" + os.system(copyCommand) + startPserver = 'nohup paddle pserver' + \ + " --port=" + str(PADDLE_PORT) + \ + " --ports_num=" + str(PADDLE_PORTS_NUM) + \ + " --ports_num_for_sparse=" + str(PADDLE_PORTS_NUM_SPARSE) + \ + " --nics=" + PADDLE_NIC + \ + " --comment=" + "paddle_process_by_paddle" + \ + " --num_gradient_servers=" + str(PADDLE_SERVER_NUM) +\ + " > " + logDir + "/server.log 2>&1 &" + print startPserver + os.system(startPserver) + # wait until pservers completely start + time.sleep(10) + startTrainer = program + args + " > " + \ + logDir + "/train.log 2>&1 < /dev/null" + print startTrainer + os.system(startTrainer) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(prog="start_paddle.py", + description='simple tool for k8s') + args, train_args_list = parser.parse_known_args() + train_args = refine_unknown_args(train_args_list) + train_args_dict = dict(zip(train_args[:-1:2], train_args[1::2])) + podlist = getPodList() + # need to wait until all pods are running + while not isPodAllRunning(podlist): + time.sleep(10) + podlist = getPodList() + idMap = getIdMap(podlist) + startPaddle(idMap, train_args_dict)