2016年4月TensorFlow发布了0.8版本宣布支持分布式计算,这个特性,我们称之为Distributed TensorFlow。
Distributed TensorFlow虽然提供了分布式能力,可以利用服务器集群加快训练,但是还有许多缺点,比如资源无法隔离、PS进程遗留问题等等,而这些正是Kubernetes所擅长的地方。下图是总结的你需要将TensorFlow运行在Kubernetes上的理由:
对于我们来说,前期最大的用户痛点就是算法团队使用的HDFS Read性能不及预期,经过网上查找资料及我们自己简单的对比测试,发现GlusterFS可能是最适合我们的分布式存储了。因此在我们的TensorFlow on Kubernetes项目中使用GlusterFS来存放训练数据,worker将从GlusterFS中读取训练数据进行计算。
说明:
支持Between-Graph和In-Graph两种replication场景;
PS Task通过Kubernetes Deployment来部署,Worker Task通过Kubernetes Job来部署,由Kubernetes service和KubeDNS来提供服务发现;
每个TensorFlow Cluster都会通过StorageClass来Dynamic Provision PV,事先会先创建好通过Heketi对接Gluster集群的StorageClass;
GlusterFS集群通过Heketi来暴露rest api与Kubernetes进行交互,关于Heketi的部署,请参考官方文档;
每个TensorFlow Cluster会最终创建两个PV,一个用来存放训练数据(挂载到容器内/data,对应TensorFlow --data_dir配置),一个用来存储训练日志(挂载到容器内/log,对应TensorFlow --log_path配置);
每个用户会对应在Kubernetes中创建一个namespace;
会给每个用户部署一个Jupyter Notebook Deployment和Service,Service通过NodePort暴露到集群外;
有一个节点比较特殊,我们称之为User Node,这个节点通过Taint方式,保证会运行Pod,但是会通过kube-proxy来暴露集群内的service,比如上面的Jupyter Notebook service将只允许在这个节点暴露出去;
User Node节点存放着用户写的python算法,并可以通过http查看和下载这些算法文件,Between-Graph场景下,容器启动后将通过curl下载这些算法文件;
整个系统涉及以下核心Components:
TensorFlow:1.3.0
Kubernetes:1.7.4
Docker:1.12.6
Glusterfs:3.10.5
Harbor:1.1.2
Contiv netplugin:0.1-12-23-2016.19-44-42.UTC
Keepalived:1.3.5
Haproxy:1.7.8
Etcd2:2.3.7
Etcd3:3.2.1
网络方案:contiv netplugin + ovs + vlan
日志方案:fluentd + Kafka + ES + Kibana监控方案:cadvisor + prometheus + GrafanaCaaS的细节不在这里讨论,其实也是大家非常熟悉的方案了。大家可以参考Kyle Bai https://github.com/kairen/workshop413,他这里用NFS作为后端存储,需要改成你们自己的存储,大家自己去尝试吧,我这就不一步一步来了,好无聊。
Q:worker是无状态的,ps是有状态的,而ps是无法进行checkpoint的,如何进行训练save和restore呢?
A:worker虽然是无状态的,但是tf.train.Saver提供能力在worker上进行checkpoint,大概原理就是逐个从PS task中get Parameters,并进行save持久化。
Q:怎么让用户指定ps和worker个数等少量参数,自动生成kubernetes yaml?
A:因为当前我们还没有针对TaaS做前端Portal,所以目前是通过jinja template来自动生成的(可以参考tensorflow/ecosystem/kubernetes),用户只要指定少量参数即可生成ps和worker需要的kubernetes yaml。
比如下面是一个例子,tfcluster_template.yaml.jinja,用户只需关注前面的4项即可。
{%- set name = "imagenet" -%} # 算法名称
{%- set worker_replicas = 3 -%} # worker数
{%- set ps_replicas = 2 -%} # ps数
{%- set script = "http://xxx.xx.xx.xxx:80/imagenet/imagenet.py" -%} # 算法脚本的http下载路径
{%- set image = "tensorflow/tensorflow:1.3.0" -%}
{%- set data_dir = "/data" -%}
{%- set log_dir = "/log" -%}
{%- set port = 2222 -%}
{%- set replicas = {"worker": worker_replicas, "ps": ps_replicas} -%}
{%- macro worker_hosts() -%}
{%- for i in range(worker_replicas) -%}
{%- if not loop.first -%},{%- endif -%}
{{ name }}-worker-{{ i }}:{{ port }}
{%- endfor -%}
{%- endmacro -%}
{%- macro ps_hosts() -%}
{%- for i in range(ps_replicas) -%}
{%- if not loop.first -%},{%- endif -%}
{{ name }}-ps-{{ i }}:{{ port }}
{%- endfor -%}
{%- endmacro -%}
{%- for job in ["worker", "ps"] -%}
{%- for i in range(replicas[job]) -%}
kind: Service
apiVersion: v1
metadata:
name: {{ name }}-{{ job }}-{{ i }}
spec:
selector:
name: {{ name }}
job: {{ job }}
task: "{{ i }}"
ports:
- port: {{ port }}
targetPort: 2222
{% if job == "worker" %}
---
kind: Job
apiVersion: batch/v1
metadata:
name: {{ name }}-{{ job }}-{{ i }}
spec:
replicas: 1
template:
metadata:
labels:
name: {{ name }}
job: {{ job }}
task: "{{ i }}"
spec:
containers:
- name: {{ name }}-{{ job }}-{{ i }}
image: {{ image }}
ports:
- containerPort: 2222
command: ["/bin/sh", "-c"]
args:["
curl {{ script }} -o /opt/{{ name }}.py;
python /opt/{{ name }}.py \
--ps_hosts={{ ps_hosts() }} \
--worker_hosts={{ worker_hosts() }} \
--job_name={{ job }} \
--task_index={{ i }} \
--log_path={{ log_dir }} \
--data_dir={{ data_dir }} ;"]
volumeMounts:
- name: data
mountPath: {{ data_dir }}
- name: log
mountPath: {{ log_dir }}
restartPolicy: Never
volumes:
- name: data
persistentVolumeClaim:
claimName: {{ name }}-data-pvc
- name: log
persistentVolumeClaim:
claimName: {{ name }}-log-pvc
{% endif %}
{% if job == "ps" %}
---
kind: Deployment
apiVersion: extensions/v1beta1
metadata:
name: {{ name }}-{{ job }}-{{ i }}
spec:
replicas: 1
template:
metadata:
labels:
name: {{ name }}
job: {{ job }}
task: "{{ i }}"
spec:
containers:
- name: {{ name }}-{{ job }}-{{ i }}
image: {{ image }}
ports:
- containerPort: 2222
command: ["/bin/sh", "-c"]
args:["
curl {{ script }} -o /opt/{{ name }}.py;
python /opt/{{ name }}.py \
--ps_hosts={{ ps_hosts() }} \
--worker_hosts={{ worker_hosts() }} \
--job_name={{ job }} \
--task_index={{ i }} \
--log_path={{ log_dir }} ;"]
volumeMounts:
- name: log
mountPath: {{ log_dir }}
restartPolicy: Never
volumes:
- name: log
persistentVolumeClaim:
claimName: {{ name }}-log-pvc
{% endif %}
---
{% endfor %}
{%- endfor -%}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ name }}-log-pvc
annotations:
volume.beta.kubernetes.io/storage-class: glusterfs
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ name }}-data-pvc
annotations:
volume.beta.kubernetes.io/storage-class: glusterfs
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Gi
---
然后执行 python rendertemplate.py tfclustertemplate.yaml.jinja | kubectl apply -f - 完成对应的Between-Graph TensorFlow Cluster的创建和启动。
相关链接:
https://arxiv.org/abs/1701.06538
https://github.com/tensorflow/tensorflow/issues/4713
本文转载自公众号: vivo互联网技术,ID:vivoVMIC,点击阅读原文。
本次培训内容包含:Kubernetes架构、安装、深入了解Kubernetes、Kubernetes高阶——设计与实现、Kubernetes落地实践、微服务、Cloud Native等,点击识别下方二维码加微信好友了解具体培训内容。