From eeca7be7ad0ca24d909b2139acb021f08f75dfb2 Mon Sep 17 00:00:00 2001 From: chenguoyan01 Date: Sun, 20 Nov 2016 10:04:43 +0800 Subject: [PATCH] fix some md syntax error. --- .../k8s/distributed_training_on_kubernetes.md | 157 ++++++++++++------ doc_cn/cluster/k8s/k8s-paddle-arch.png | Bin 0 -> 22084 bytes 2 files changed, 105 insertions(+), 52 deletions(-) create mode 100644 doc_cn/cluster/k8s/k8s-paddle-arch.png diff --git a/doc_cn/cluster/k8s/distributed_training_on_kubernetes.md b/doc_cn/cluster/k8s/distributed_training_on_kubernetes.md index 8e947f8d56a..e07cf6182f2 100644 --- a/doc_cn/cluster/k8s/distributed_training_on_kubernetes.md +++ b/doc_cn/cluster/k8s/distributed_training_on_kubernetes.md @@ -1,84 +1,75 @@ # 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集群上启动一个单机PaddlePaddle训练作业 (Job)。在这篇文章里,我们介绍如何在Kubernetes集群上进行分布式PaddlePaddle训练作业。关于PaddlePaddle的分布式训练,可以参考 [Cluster Training](https://github.com/baidu/Paddle/blob/develop/doc/cluster/opensource/cluster_train.md),本文利用Kubernetes的调度功能与容器编排能力,快速构建PaddlePaddle容器集群,进行分布式训练任务。 ## Kubernetes 基本概念 -在介绍分布式训练之前,需要对Kubernetes(k8s)有一个基本的认识,下面先简要介绍一下本文用到的几个k8s概念。 +[*Kubernetes*](http://kubernetes.io/)是Google开源的容器集群管理系统,其提供应用部署、维护、 扩展机制等功能,利用Kubernetes能方便地管理跨机器运行容器化的应用。在介绍分布式训练之前,需要对[Kubernetes](http://kubernetes.io/)有一个基本的认识,下面先简要介绍一下本文用到的几个Kubernetes概念。 -### Node +- [*Node*](http://kubernetes.io/docs/admin/node/) 表示一个Kubernetes集群中的一个工作节点,这个节点可以是物理机或者虚拟机,Kubernetes集群就是由node节点与master节点组成的。 -[`Node`](http://kubernetes.io/docs/admin/node/) 表示一个k8s集群中的一个工作节点,这个节点可以是物理机或者虚拟机,k8s集群就是由`node`节点与`master`节点组成的。每个node都安装有Docker,在本文的例子中,`Paadle`容器就在node上运行。 +- [*Pod*](http://kubernetes.io/docs/user-guide/pods/) 是一组(一个或多个)容器,pod是Kubernetes的最小调度单元,一个pod中的所有容器会被调度到同一个node上。Pod中的容器共享NET,PID,IPC,UTS等Linux namespace。由于容器之间共享NET namespace,所以它们使用同一个IP地址,可以通过*localhost*互相通信。不同pod之间可以通过IP地址访问。 -### Pod +- [*Job*](http://kubernetes.io/docs/user-guide/jobs/) 是Kubernetes上运行的作业,一次作业称为一个job,通常每个job包括一个或者多个pods。 -一个[`Pod`](http://kubernetes.io/docs/user-guide/pods/) 是一组(一个或多个)容器,pod是k8s的最小调度单元,一个pod中的所有容器会被调度到同一个node上。Pod中的容器共享NET,PID,IPC,UTS等Linux namespace,它们使用同一个IP地址,可以通过`localhost`互相通信。不同pod之间可以通过IP地址访问。 +- [*Volume*](http://kubernetes.io/docs/user-guide/volumes/) 存储卷,是pod内的容器都可以访问的共享目录,也是容器与node之间共享文件的方式,因为容器内的文件都是暂时存在的,当容器因为各种原因被销毁时,其内部的文件也会随之消失。通过volume,就可以将这些文件持久化存储。Kubernetes支持多种volume,例如hostPath(宿主机目录),gcePersistentDisk,awsElasticBlockStore等。 -### 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`默认命名空间,读者可以不关心此概念。 +- [*Namespaces*](http://kubernetes.io/docs/user-guide/volumes/) 命名空间,在kubernetes中创建的所有资源对象(例如上文的pod,job)等都属于一个命名空间,在同一个命名空间中,资源对象的名字是唯一的,不同空间的资源名可以重复,命名空间主要为了对象进行逻辑上的分组便于管理。本文只使用了默认命名空间。 ## 整体方案 -### 前提条件 +### 部署Kubernetes集群 + +首先,我们需要拥有一个Kubernetes集群,在这个集群中所有node与pod都可以互相通信。关于Kubernetes集群搭建,可以参考[官方文档](http://kubernetes.io/docs/getting-started-guides/kubeadm/),在以后的文章中我们也会介绍AWS上搭建的方案。本文假设大家能找到几台物理机,并且可以按照官方文档在上面部署Kubernetes。在本文的环境中,Kubernetes集群中所有node都挂载了一个*mfs*(分布式文件系统)共享目录,我们通过这个目录来存放训练文件与最终输出的模型。在训练之前,用户将配置与训练数据切分好放在mfs目录中,训练时,程序从此目录拷贝文件到容器内进行训练,将结果保存到此目录里。整体的结果图如下: -首先,我们需要拥有一个k8s集群,在这个集群中所有node与pod都可以互相通信。关于k8s集群搭建,可以参考[官方文档](http://kubernetes.io/docs/getting-started-guides/kubeadm/),在以后的文章中我们也会介绍AWS上搭建的方案。在本文的环境中,k8s集群中所有node都挂载了一个`mfs`(分布式文件系统)共享目录,我们通过这个目录来存放训练文件与最终输出的模型。在训练之前,用户将配置与训练数据切分好放在mfs目录中,训练时,程序从此目录拷贝文件到容器内进行训练,将结果保存到此目录里。 +![paddle on kubernetes结构图](k8s-paddle-arch.png) -### 使用 `Job` +### 使用 Job -我们使用k8s中的job这个概念来代表一次分布式训练。`Job`表示一次性作业,在作业完成后,k8s会销毁job产生的容器并且释放相关资源。 +我们使用Kubernetes中的job这个概念来代表一次分布式训练。Job表示一次性作业,在作业完成后,Kubernetes会销毁job产生的容器并且释放相关资源。 -在k8s中,可以通过编写一个 `yaml` 文件,来描述这个job,在这个文件中,主要包含了一些配置信息,例如Paddle节点的个数,`paddle pserver`开放的端口个数与端口号,`paddle`使用的网卡设备等,这些信息通过环境变量的形式传递给容器内的程序使用。 +在Kubernetes中,可以通过编写一个YAML文件,来描述这个job,在这个文件中,主要包含了一些配置信息,例如PaddlePaddle的节点个数,`paddle pserver`开放的端口个数与端口号,使用的网卡设备等,这些信息通过环境变量的形式传递给容器内的程序使用。 -在一次分布式训练中,用户确定好本次训练需要的Paddle节点个数,将切分好的训练数据与配置文件上传到`mfs`共享目录中。然后编写这次训练的`job yaml`文件,提交给k8s集群创建并开始作业。 +在一次分布式训练中,用户确定好本次训练需要的PaddlePaddle节点个数,将切分好的训练数据与配置文件上传到mfs共享目录中。然后编写这次训练的job YAML文件,提交给Kubernetes集群创建并开始作业。 -### 创建`Paddle`节点 +### 创建PaddlePaddle节点 -当k8s master收到`job yaml`文件后,会解析相关字段,创建出多个pod(个数为Paddle节点数),k8s会把这些pod调度到集群的node上运行。一个`pod`就代表一个`Paddle`节点,当pod被成功分配到一台物理/虚拟机上后,k8s会启动pod内的容器,这个容器会根据`job yaml`文件中的环境变量,启动`paddle pserver`与`paddle train`进程。 +当Kubernetes master收到请求,解析完YAML文件后,会创建出多个pod(个数为PaddlePaddle节点数),Kubernetes会把这些pod调度到集群的node上运行。一个pod就代表一个PaddlePaddle节点,当pod被成功分配到一台物理/虚拟机上后,Kubernetes会启动pod内的容器,这个容器会根据YAML文件中的环境变量,启动`paddle pserver`与`paddle train`进程。 ### 启动训练 -在容器启动后,会通过脚本来启动这次分布式训练,我们知道`paddle train`进程启动时需要知道其他节点的IP地址以及本节点的`trainer_id`,由于`Paddle`本身不提供类似服务发现的功能,所以在本文的启动脚本中,每个节点会根据`job name`向`k8s apiserver`查询这个`job`对应的所有`pod`信息(k8s默认会在每个容器的环境变量中写入`apiserver`的地址)。 +在容器启动后,会通过脚本来启动这次分布式训练,我们知道`paddle train`进程启动时需要知道其他节点的IP地址以及本节点的trainer_id,由于Paddle本身不提供类似服务发现的功能,所以在本文的启动脚本中,每个节点会根据job name向Kubernetes apiserver查询这个job对应的所有pod信息(Kubernetes默认会在每个容器的环境变量中写入apiserver的地址)。 -根据这些pod信息,就可以通过某种方式,为每个pod分配一个唯一的`trainer_id`。本文把所有pod的IP地址进行排序,将顺序作为每个`Paddle`节点的`trainer_id`。启动脚本的工作流程大致如下: +根据这些pod信息,就可以通过某种方式,为每个pod分配一个唯一的trainer_id。本文把所有pod的IP地址进行排序,将顺序作为每个PaddlePaddle节点的trainer_id。启动脚本的工作流程大致如下: - 1. 查询`k8s apiserver`获取pod信息,根据IP分配`trainer_id` - 1. 从`mfs`共享目录中拷贝训练文件到容器内 + 1. 查询Kubernetes apiserver获取pod信息,根据IP分配trainer_id + 1. 从mfs共享目录中拷贝训练文件到容器内 1. 根据环境变量,解析出`paddle pserver`与`paddle train`的启动参数,启动进程 - 1. 训练时,`Paddle`会自动将结果保存在`trainer_id`为0的节点上,将输出路径设置为`mfs`目录,保存输出的文件 + 1. 训练时,PaddlePaddle会自动将结果保存在trainer_id为0的节点上,将输出路径设置为mfs目录,保存输出的文件 ## 搭建过程 -根据前文的描述,要在已有的k8s集群上进行`Paddle`的分布式训练,主要分为以下几个步骤: +根据前文的描述,要在已有的Kubernetes集群上进行PaddlePaddle的分布式训练,主要分为以下几个步骤: -1. 制作`Paddle`镜像 +1. 制作PaddlePaddle镜像 1. 将训练文件与切分好的数据上传到共享存储 -1. 编写本次训练的`job yaml`文件,创建`k8s job` +1. 编写本次训练的YAML文件,创建一个Kubernetes job 1. 训练结束后查看输出结果 下面就根据这几个步骤分别介绍。 - ### 制作镜像 -`Paddle`镜像需要提供`paddle pserver`与`paddle train`进程的运行环境,用这个镜像创建的容器需要有以下两个功能: +PaddlePaddle镜像需要提供`paddle pserver`与`paddle train`进程的运行环境,用这个镜像创建的容器需要有以下两个功能: - 拷贝训练文件到容器内 - 生成`paddle pserver`与`paddle train`进程的启动参数,并且启动训练 -因为官方镜像 `paddledev/paddle:cpu-latest` 内已经包含`Paddle`的执行程序但是还没上述功能,所以我们可以在这个基础上,添加启动脚本,制作新镜像来完成以上的工作。镜像的`Dockerfile`如下: +因为官方镜像 `paddledev/paddle:cpu-latest` 内已经包含PaddlePaddle的执行程序但是还没上述功能,所以我们可以在这个基础上,添加启动脚本,制作新镜像来完成以上的工作。镜像的*Dockerfile*如下: ```Dockerfile FROM paddledev/paddle:cpu-latest @@ -92,25 +83,87 @@ CMD ["bash"," -c","/root/start.sh"] [`start.sh`](start.sh)文件拷贝训练文件到容器内,然后执行[`start_paddle.py`](start_paddle.py)脚本启动训练,前文提到的获取其他节点IP地址,分配`trainer_id`等都在`start_paddle.py`脚本中完成。 +`start_paddle.py`脚本开始时,会先进行参数的初始化与解析。 + +```python +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() +``` + +然后通过函数`getPodList()`访问Kubernetes的接口来查询此job对应的所有pod信息。当所有pod都处于running状态(容器运行都运行)时,再通过函数`getIdMap(podlist)`获取trainer_id。 + +```python + podlist = getPodList() + # need to wait until all pods are running + while not isPodAllRunning(podlist): + time.sleep(10) + podlist = getPodList() + idMap = getIdMap(podlist) +``` + +在函数`getIdMap(podlist)`内部,我们通过读取`podlist`中每个pod的IP地址,将IP排序生成的序号作为trainer_id。 + +```python +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 +``` + +在得到`idMap`后,通过函数`startPaddle(idMap, train_args_dict)`构造`paddle pserver`与`paddle train`的启动参数并执行进程。 + +在函数`startPaddle`中,最主要的工作就是解析出`paddle pserver`与`paddle train`的启动参数。例如`paddle train`参数的解析,解析环境变量得到`PADDLE_NIC`,`PADDLE_PORT`,`PADDLE_PORTS_NUM`等参数,然后通过自身的IP地址在`idMap`中获取`trainerId`。 + +```python + 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 +``` 使用 `docker build` 构建镜像: ```bash -docker build -t registry.baidu.com/public/paddle:mypaddle . +docker build -t your_repo/paddle:mypaddle . ``` -然后将构建成功的镜像上传到镜像仓库,注意本文中使用的`registry.baidu.com`是一个私有仓库,读者可以根据自己的情况部署私有仓库或者使用`Docker hub`。 +然后将构建成功的镜像上传到镜像仓库。 ```bash -docker push registry.baidu.com/public/paddle:mypaddle +docker push your_repo/paddle:mypaddle ``` ### 上传训练文件 -本文使用`Paddle`官方的`recommendation demo`作为这次训练的内容,我们将训练文件与数据放在一个`job name`命名的目录中,上传到`mfs`共享存储。完成后`mfs`上的文件内容大致如下: +本文使用Paddle官方的[recommendation demo](http://www.paddlepaddle.org/doc/demo/index.html#recommendation)作为这次训练的内容,我们将训练文件与数据放在一个job name命名的目录中,上传到mfs共享存储。完成后mfs上的文件内容大致如下: ```bash -[root@paddle-k8s-node0 mfs]# tree -d +[root@paddle-kubernetes-node0 mfs]# tree -d . └── paddle-cluster-job ├── data @@ -123,13 +176,13 @@ docker push registry.baidu.com/public/paddle:mypaddle └── recommendation ``` -目录中`paddle-cluster-job`是本次训练对应的`job name`,本次训练要求有3个`Paddle`节点,在`paddle-cluster-job/data`目录中存放切分好的数据,文件夹`0,1,2`分别代表3个节点的`trainer_id`。`recommendation`文件夹内存放训练文件,`output`文件夹存放训练结果与日志。 +目录中paddle-cluster-job是本次训练对应的job name,本次训练要求有3个Paddle节点,在paddle-cluster-job/data目录中存放切分好的数据,文件夹0,1,2分别代表3个节点的trainer_id。recommendation文件夹内存放训练文件,output文件夹存放训练结果与日志。 -### 创建`job` +### 创建Job -`k8s`可以通过`yaml`文件来创建相关对象,然后可以使用命令行工具创建`job`。 +Kubernetes可以通过YAML文件来创建相关对象,然后可以使用命令行工具创建job。 -`job yaml`文件描述了这次训练使用的Docker镜像,需要启动的节点个数以及 `paddle pserver`与 `paddle train`进程启动的必要参数,也描述了容器需要使用的存储卷挂载的情况。`yaml`文件中各个字段的具体含义,可以查看[`k8s官方文档`](http://kubernetes.io/docs/api-reference/batch/v1/definitions/#_v1_job)。例如,本次训练的`yaml`文件可以写成: +Job YAML文件描述了这次训练使用的Docker镜像,需要启动的节点个数以及 `paddle pserver`与 `paddle train`进程启动的必要参数,也描述了容器需要使用的存储卷挂载的情况。YAML文件中各个字段的具体含义,可以查看[Kubernetes Job API](http://kubernetes.io/docs/api-reference/batch/v1/definitions/#_v1_job)。例如,本次训练的YAML文件可以写成: ```yaml apiVersion: batch/v1 @@ -149,7 +202,7 @@ spec: path: /home/work/mfs containers: - name: trainer - image: registry.baidu.com/public/paddle:mypaddle + image: your_repo/paddle:mypaddle command: ["bin/bash", "-c", "/root/start.sh"] env: - name: JOB_NAME @@ -176,7 +229,7 @@ spec: restartPolicy: Never ``` -文件中,`metadata`下的`name`表示这个`job`的名字。`parallelism,completions`字段表示这个`job`会同时开启3个`Paddle`节点,成功训练且退出的`pod`数目为3时,这个`job`才算成功结束。然后申明一个存储卷`jobpath`,代表宿主机目录`/home/work/mfs`,在对容器的描述`containers`字段中,将此目录挂载为容器的`/home/jobpath`目录,这样容器的`/home/jobpath`目录就成为了共享存储,放在这个目录里的文件其实是保存到了`mfs`上。 +文件中,`metadata`下的`name`表示这个job的名字。`parallelism,completions`字段表示这个job会同时开启3个Paddle节点,成功训练且退出的pod数目为3时,这个job才算成功结束。然后申明一个存储卷`jobpath`,代表宿主机目录`/home/work/mfs`,在对容器的描述`containers`字段中,将此目录挂载为容器的`/home/jobpath`目录,这样容器的`/home/jobpath`目录就成为了共享存储,放在这个目录里的文件其实是保存到了mfs上。 `env`字段表示容器的环境变量,我们将`paddle`运行的一些参数通过这种方式传递到容器内。 @@ -190,21 +243,21 @@ spec: `CONF_PADDLE_GRADIENT_NUM`表示训练节点数量,即`--num_gradient_servers`参数 -编写完`yaml`文件后,可以使用k8s的命令行工具创建`job`. +编写完YAML文件后,可以使用Kubernetes的命令行工具创建job。 ```bash kubectl create -f job.yaml ``` -创建成功后,k8s就会创建3个`pod`作为`Paddle`节点然后拉取镜像,启动容器开始训练。 +创建成功后,Kubernetes就会创建3个pod作为PaddlePaddle节点然后拉取镜像,启动容器开始训练。 ### 查看输出 -在训练过程中,可以在共享存储上查看输出的日志和模型,例如`output`目录下就存放了输出结果。注意`node_0`,`node_1`,`node_2`这几个目录表示`Paddle`节点与`trainer_id`,并不是k8s中的`node`概念。 +在训练过程中,可以在共享存储上查看输出的日志和模型,例如output目录下就存放了输出结果。注意node_0,node_1,node_2这几个目录表示Paddle节点与trainer_id,并不是Kubernetes中的node概念。 ```bash -[root@paddle-k8s-node0 output]# tree -d +[root@paddle-kubernetes-node0 output]# tree -d . ├── node_0 │   ├── server.log @@ -224,7 +277,7 @@ kubectl create -f job.yaml 我们可以通过日志查看容器训练的情况,例如: ```bash -[root@paddle-k8s-node0 node_0]# cat train.log +[root@paddle-kubernetes-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 diff --git a/doc_cn/cluster/k8s/k8s-paddle-arch.png b/doc_cn/cluster/k8s/k8s-paddle-arch.png new file mode 100644 index 0000000000000000000000000000000000000000..a8c64550b1fa7f41de1eaa9a037c65cddc0cd30e GIT binary patch literal 22084 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�gYl^6l-ltkTlmdD|aM=)lE&m0c9Ljcj^BL7lC-z2L>3r2HR0o`S5yv$fIU z(Ljy{fR*V8?ml`NzCU4E3@Tu^MghIeYibgs5OPxjuhw}@!ls?k06$%=1#iA_y#)3t z;IW^h?|p21<;s;|L6cuUK7hPkewHGl2+j8iMUGPF%rDN4C(2Ail6@}Rg~2{g-rJ{z{~1EjGA5=6N?hjSsKMb2|Q zAc<=Mt|OpqlmbNH1hrKR76GKj0R~k^bIV_IbHfdAPht|1Rq#@tNHzFzCz|8ed!kl> zUjR9H4SV~~X!({vqeYCa!-r3t==~(%x>>N$8MD@Kx+!Q5+@~8k+KJj}Z`SQ8fT^vt zklPLc*5Pz{j$&bClu4Hg0==4zY%!ggWjzAW5pJy2(c7yDh@$Y_9^3DdbUI#Vh7y^zS=ItIwCM6Z3 z32<}s%HgDK%=e#t?+D023RQMtVIkL`(Ho>9(`^r z+P|SrTvqOq$}LH{U=7uY6>DnJ zDNPtd7?);Qsiw2JG%*borLhPl7P%z$`Px5Vf8P0Newg?BzTfwAd7jVndFKs#*L`P1 zdi_3g6ad6k12Uy_vL-X5+YC66HSDvoUlGi3W7%6cL~#3+;Zo3gG69f?b;&}--a~(yFaIpy38yE@gjx`daI;5xsY9|tg5+f`*s$*xqK!=FC!y^XTIgxZu-xy`6j$4 zbUIyx^txs9=FrK(HWd|>4tEcB5yZv_KoMag|cPB=Px_k+{{P`xDn2YQolmS^R0@O&P#&ZO9>?ShWvT$VJS;*%lj-1Ooy_e?w^+cbwv9Tc_@^En%Fs`N20K!=@Y%j@IkIDgmfFsK1i+-=HWI@>xjn%GEQ!8{Zs=s;fvrKy^ zT_^eWq-yd>Vg2BS&yzSYWpMaqjgw!}gk71*m#ej<@19o;9IcmCg}AhFX)an?T3dVL z1BrZtE|uJGY2Je!_dJOADdY=f<**6G!+DUj+f`4j4wN^}mtY%kj*lPSe#MM);lh5r zAxc+)6L%+$CJIZcK1@x;;3;ES&s0czj^Qu=BvM6c%RmQ)m%^G?A;}Wr-C3Ffa;&sD zRdpIy>gZFcBKb+`&OETY!|P6&Q1W6>V=eXdh1ez9vI=&E65xu8p|( z0qmi;xR}t2k;0|#UbTm-4|RbMr{5BQc^ptvM#3wm^6BY~(EQ&PH~we{=u zfvv9>Khl8A0T9AQEzCc5b#!)GUR(P6k`ORo=VSCFf{-6GIGpY3Tb*}+aR@NI$?))S zFt{%-P$H2O;0F(~Hc%fU>hC+|8{f0Fx96s+t}B37nLn~hUZ`U_IGBQ|)z(AM#0^jx zL!z`sooUOCSY)N(+3`2QT|)LPfuL1;cU-HsRZ{mi+PkEtfTs?}tLD#ojdz?c87 zC&$0FX0nbSukVd*a*-_vKq@bH`>2o#m34vVP1Z@eOvF`%c=hb*jt-WF286YHz{BK< z<$3K;B&3|`{+>x`n9Am4X1*6H8+szip%HR0>ypW!rFe7&sXFpC;AC&!&rls`Rkw*9LgUHl}%%4bpBhmBr8Tr%g3VS zULlr;vodnlM#fu)-g&&aj4D}}5wgXNwc_>I2nMmBr6cFk)6<#eU1K)VO3%x&xFV0T nXk=*E78GxYJ^4R=n!@Hzow=b{nG)e=My#J{=WHvq@jvqqm+3b2 literal 0 HcmV?d00001 -- GitLab