全栈工程师开发手册 (作者:栾鹏)
一站式云原生机器学习平台
github:https://github.com/tencentmusic/cube-studio
前言:cube studio是tme开源的云原生机器学习平台,目前包含特征平台,支持在/离线特征;数据源管理,支持结构数据和媒体标注数据管理;在线开发,在线的vscode/jupyter代码开发;在线镜像调试,支持免dockerfile,增量构建;任务流编排,在线拖拉拽;开放的模板框架,支持tf/pytorch/spark/ray/horovod/kaldi等分布式训练任务;task的单节点debug,分布式任务的批量优先级调度,聚合日志;任务运行资源监控,报警;定时调度,支持补录,忽略,重试,依赖,并发限制,定时任务算力的智能修正;nni,katib,ray的超参搜索;多集群多资源组,算力统筹,联邦调度;tf/pytorch/onnx模型的推理服务,serverless流量管控,tensorrt gpu推理加速,依据gpu利用率/qps等指标的 hpa能力,虚拟化gpu,虚拟显存等服务化能力。
cube中包含多种类型的服务入口:平台本身的web服务,notebook的web服务,nni超参搜索的web服务,离线模型以包管理形式的推理服务,embedding服务,k8s的云原生服务,以及云原生的模型推理服务。
推理服务各阶段
推理服务从底层到上层,包含服务网格,serverless,pipeline,http框架,模型计算。
服务网格阶段:主要工作是代理流量的中转和管控,例如分流,镜像,限流,黑白名单之类的。
serverless阶段:主要为服务的智能化运维,例如服务的激活,伸缩容,版本管理,蓝绿发布。
pipeline阶段:主要为请求在各数据处理/推理之间的流动。推理的前后置处理逻辑等。
http/grpc框架:主要为处理客户端的请求,准备推理样本,推理后作出响应。
模型计算:模型在cpu/gpu上对输入样本做前向计算。
网关入口适配
在最开始平台提供的统一网关入口是使用k8s nginx ingress+istio ingressgateway,在开源后,统一方案,全部改为istio ingressgateway的网关方案。这里说一下两者的使用体验。
在ingress可以代理不同的域名,匹配不同的url prefix,分配流量比例,优势在于配置简单,k8s自带的功能,也不需要额外的组件。另外从nginx移植过来的功能比较方便。可以在annotations添加很多原有配置,甚至可以改写response响应。
kind: Ingress
metadata:
name: xx
annotations:
nginx.ingress.kubernetes.io/proxy-connect-timeout: "300"
nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
nginx.ingress.kubernetes.io/proxy-body-size: 50m
nginx.ingress.kubernetes.io/cors-allow-methods: "GET, PUT, POST, DELETE, PATCH, OPTIONS"
nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/cors-allow-credentials: "true"
nginx.ingress.kubernetes.io/cors-allow-headers: "true"
nginx.ingress.kubernetes.io/cors-expose-headers: "*, X-CustomResponseHeader"
nginx.ingress.kubernetes.io/cors-allow-origin: "*"
# nginx server部分
# nginx.ingress.kubernetes.io/server-snippet: |
# nginx location部分
nginx.ingress.kubernetes.io/configuration-snippet: |
more_set_headers "Access-Control-Allow-Origin: *";
more_set_headers "Access-Control-Allow-Methods: *";
more_set_headers "Access-Control-Allow-Headers: *";
proxy_set_header Accept-Encoding "";
sub_filter '<head>' '<head><script src="http://xx.xx.xx/xx/xx.js"></script>';
而使用istio ingressgateway也可以实现比nginx ingress更多的功能配置,底层的代理服务为envoy。目前cube开源版本全部为istio ingressgateway代理入口,istio的好处是能将外部服务加入到服务网格中,在serverless中也能很好的使用。不好的地方莫过于istio没有k8s ingress那个简单,ingress只是网关,而isito定位是服务网格。
cube中的很多功能是通过外部镜像镜像实现的,比如jupyter,theia,nni,k8s-dashboard,kfp,云原生服务用户自己定义的镜像,以及推理服务外部各框架镜像,kfserving镜像等。他们在网关入口处基本通过url prefix或者host进行的区分代理。内部再加上泛化域名的能力,这样就能部署后直接访问了。
serverless
服务网格和serverless基本方案在前面的文章介绍过了
分流+流量复制
分流表示将一批流量按照一定规则分发到多个服务上去,每一个请求只会进入一个服务端,流量镜像则表示将原有的流量原封不动的复制一份到另一个服务。这两种功能在实际生产中都非常有用。分流比较好理解,流量复制如下图所示。发起端是不等待镜像服务的响应的。
在isito中是通过虚拟服务(Virtual Service) 和目标规则(Destination Rule)来管理流量的策略的。他们与k8s的关系如下图
在istio中可以很方便的实现流量的分发和复制。cube里面一个模型使用一个唯一的pod,不会在一个pod中进行多个模型的推理服务,所以在服务网格中流量的复制和分流就更加简单,在VirtualService中进行流量复制的方式如下
http:
- mirror:
host: service1-version1.kfserving.svc.cluster.local
port:
number: 8501
mirror_percent: 100
route:
- destination:
host: service1-version2.kfserving.svc.cluster.local
port:
number: 8501
timeout: 300s
在很多情况下,我们需要将真实的流量数据与镜像流量数据进行收集并分析,需要在其中将各个复制端的请求和结果导入到对比系统中进行比较查看服务的好坏。
在VirtualService中进行流量分发的方式如下:
http:
- route:
- destination:
host: service1-version1.kfserving.svc.cluster.local
port:
number: 8501
weight: 20
- destination:
host: service1-version2.kfserving.svc.cluster.local
port:
number: 8501
weight: 80
timeout: 300s
激活器
激活器的主要目的为在用户允许推理服务副本伸缩到0的时候如何在不中断请求的情况下将服务部署起来。这对于那些一定时间段内没有任何请求的服务是比较有意义的。可以在没有请求的时候及时将资源释放出来。
prometheus服务发现
在k8s中由于服务pod的ip是变化的,所以不能像在主机上一样固定写死pod的ip。所以在k8s中需要服务的自动发现。在原有的方案中需要为服务注册ServiceMonitor,当需要监控的服务比较多时配置起来可能会有遗忘,或比较麻烦,所以可以通过kubernetes_sd_configs实现对k8s各种资源的自动监控。
# 其中通过 kubernetes_sd_configs 支持监控其各种资源
- job_name: 'kubernetes-service-endpoints'
kubernetes_sd_configs:
- role: endpoints
relabel_configs:
# 重新打标仅抓取到的具有 "prometheus.io/scrape: true" 的annotation的端点,意思是说如果某个service具有prometheus.io/scrape = true annotation声明则抓取
# annotation本身也是键值结构,所以这里的源标签设置为键,而regex设置值,当值匹配到regex设定的内容时则执行keep动作也就是保留,其余则丢弃.
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape]
action: keep
regex: true
# 匹配源标签__meta_kubernetes_service_annotation_prometheus_io_scheme也就是prometheus.io/scheme annotation
# 如果源标签的值匹配到regex则把值替换为__scheme__对应的值
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scheme]
action: replace
target_label: __scheme__
regex: (https?)
# 获取service的 annotation 中定义的"prometheus.io/path: XXX"定义的值,这个值就是你的程序暴露符合prometheus规范的metrics的地址
# 如果你的metrics的地址不是 /metrics 的话,通过这个标签说,那么这里就会把这个值赋值给 __metrics_path__这个变量,
# 因为prometheus是通过这个变量获取路径然后进行拼接出来一个完整的URL,并通过这个URL来获取metrics值的,因为prometheus默认使用的就是 http(s)://X.X.X.X/metrics这样一个路径来获取的。
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
- source_labels: [__address__, __meta_kubernetes_service_annotation_prometheus_io_port]
action: replace
target_label: __address__
regex: ([^:]+)(?::\d+)?;(\d+)
replacement: $1:$2
# 下面主要是为了给样本添加额外信息
- action: labelmap
regex: __meta_kubernetes_service_label_(.+)
- source_labels: [__meta_kubernetes_namespace]
action: replace
target_label: kubernetes_namespace
- source_labels: [__meta_kubernetes_service_name]
action: replace
target_label: kubernetes_name
这一部分的ScrapeConfigs配置是主动从service中发现annotations中的下列信息
"prometheus.io/scrape": "true",
"prometheus.io/port": 'xxx'
"prometheus.io/path": 'xxx'
这样就能自动监控此类服务下的所有pod的metrics接口了。
基于qps/gpu利用率进行hpa
k8s中hpa可以直接使用的弹性伸缩功能为根据pod的内存和cpu进行伸缩。但是在gpu上的推理,没有直接可以使用的伸缩指标。所以基于qps或者gpu利用率的伸缩容指标需要借助自定义指标来实现。
k8s上可以通过注册APIService,将外部的指标注册为k8s的metric,然后再将此指标作为deployment伸缩容的指标。我们大部分的指标都采集到prometheus,所以可以通过prometheus_adapter来将prometheus的指标注册为伸缩容的可用指标。
下面以gpu上的推理服务,GPU利用率为例。首先要能采集到pod中的gpu使用率。在k8s中使用nvidia官方的监控方案dcgm-exporter,通过此可以拿到每个在gpu上的pod的gpu利用率。
在prometheus中的指标类型为
DCGM_FI_DEV_GPU_UTIL{UUID="xx", container="dcgm-exporter", endpoint="metrics", exported_container="volcanojob", exported_namespace="xx", exported_pod="xx", gpu="0", instance="xx:9400", job="dcgm-exporter", namespace="monitoring", pod="dcgm-exporter-xx", service="dcgm-exporter"}
指标名为 DCGM_FI_DEV_GPU_UTIL,所对应的工作pod为exported_pod,工作pod所在命名空间exported_namespace,通过这两个就可以匹配到目标pod。
我们通过configmap的形式注册apiserver的指标
# gpu 利用率
- seriesQuery: '{__name__="DCGM_FI_DEV_GPU_UTIL",exported_pod!="",exported_namespace="service"}'
seriesFilters: []
# pod 自定义指标 就是hpa查询查询指定命名空间指定pod的指标。所以需要通过resources配置 指标标签和命名空间和pod名称的映射关系
resources:
overrides:
exported_namespace: # 指标的标签
resource: namespace # k8s的资源名
exported_pod: # 指标的标签
resource: pod # k8s的资源名
# 用来给指标重命名的
name:
matches: "^(.*)"
as: "container_gpu_usage"
# 获取指标的值。Series:表示指标名称,LabelMatchers:附加的标签,就是 pod 名称
metricsQuery: (sum(avg_over_time(<<.Series>>{<<.LabelMatchers>>}[5m])) by (<<.GroupBy>>))/100
主要需要关注的点为指标中的label exported_namespace为真实pod的namespace,指标中的exported_pod为真实pod的pod 名。并且在实际推理中,请求进来gpu利用率才会变高,所以为了避免太过敏感,最终用于hpa的指标值做了5分钟平均。
各类推理框架
不同的训练框架,基本都有对应的推理框架。cube为tf/torch/onnx/tensorrt/lightgbm/paddle/sklearn/xgboost等模型提供推理服务,基本仅需要用户填写模型名称和版本、模型地址,会自动生成所有的配置文件,提供api的demo,自动提供域名和L5的访问。
各类型的推理框架或多或少可以代理其他训练框架的模型进行推理服务。例如tfserving也可以为onnx模型提供推理服务,所以这些框架和模型并不是唯一绑定关系。
tfserving
tfserving主要是tf模型推理服务,虽然同时也可以为其他模型提供推理服务的,比如上面说的onnx模型也是可以用tfserving提供推理服务的。
tfserving推理主要需要提供models.config、monitoring.config、platform.config等配置文件。推理服务提供了Model status API、Model Metadata API、Classify and Regress API、Predict API等类型的接口。
可以使用Model Metadata API作为模型的健康检查接口。
monitoring.config中配置的metric可以用于prometheus监控。
torchserver
torch保存模型结构和参数完成信息后,需要先使用torch-model-archiver将模型压缩为可直接推理的包。主要是将http接口封装进去。
torch-model-archiver --model-name $model_name --version $model_version --handler image_classifier --serialized-file $model_version/$model_name --export-path $model_version -f
其中 --handler 支持如下 image_classifier,image_segmenter,object_detector,text_classifier 或自定义py函数。这个handler就是接口处理方式,也就决定了用户客户端端该如何请求。
torch server主要配置为config.properties和log4j.properties,提供的api包括推理(8080端口)、管理(8081)、监控(8082)
8080/ping可以用于pod的健康检查,POST /metrics可以用于prometheus的监控
onnxruntime
pass
模型压缩
pass
gpu推理加速
推理加速除了对模型进行处理,例如模型变小,计算量变小以外,还可以在gpu上针对gpu的计算进行并行和去重。
TensorRT是NVIDIA 推出的一款基于CUDA和cudnn的神经网络推断加速引擎,相比于一般的深度学习框架,在CPU或者GPU模式下其可提供10X乃至100X的加速,极大提高了深度学习模型的推断速度。
tensorrt的加速原理一个是支持INT8和FP16的计算,通过在减少计算量和保持精度之间达到一个理想的trade-off,另外一个是TensorRT对于网络结构进行了重构和优化,减少了不必要的计算和重复的计算。包括消除无用输出层、网络的垂直整合、网络的水平组合等方案。
因为支持在gpu计算上的加速,并不影响http的推理框架,所以tensorrt server解耦了http框架和gpu计算部分,可以兼容多种http框架,同时还支持自定义的http框架
tensorrt server的主要配置为config.pbtxt,同时还集成了一个客户端sdk,是因为兼容多种http框架的同事,每一种不同的模型,不同的http框架,不同的input/output,请求的方式不一样,需要先根据事实情况判断组合该用什么样的请求流。
vgpu
gpu 推理时三类场景需要用到gpu:
1、gpu显存和gpu的利用率低,vgpu可以在一张gpu卡上放更多的推理服务,共用一个gpu。
2、本身gpu数量有限,需要更多的gpu数量满足需求,vgpu可以虚化出更多的gpu数量
3、显存不充足,模型比较大,或者大模型却没有较多的qps,显存占用率和gpu利用率差距比较大场景。
gpu由于价格比较昂贵,所以使用占用方式比较谨慎。在k8s上原生的gpu占用的方式如下:
limits:
nvidia.com/gpu=1
这种方式是独占gpu的方式,占上以后就不会被其他pod占用。但是如果pod占用后不使用的话就会非常浪费。
还有一种占用方式是共享占用,可以被多个pod共同占用和共同使用。通过环境变量确定占用的gpu的卡号,这种方式跳过gpu的资源调度器,这样就不会被k8s识别到gpu卡被占用了,但是这种方式多个pod占用一个gpu,pod之间会相互影响。这种方式适合允许pod之间gpu能力相互干扰的情况,比如某些对成功率没有很高的场景,在notebook的gpu场景中就可以使用这种方式占用gpu。
env:
- name: NVIDIA_VISIBLE_DEVICES
value: "0,1" # 写相对卡顺序,比较方便
如果应用在tke上,也可以直接部署gpu-manager组件,使用vgpu的功能。在tke上可以占用卡的一部分,显存为vcuda-memory*256Mi, 这个是独立占用gpu核和显存两个资源
limits:
tencent.com/vcuda-core: 200
tencent.com/vcuda-memory: 60
如果是在自建k8s上,也可以使用gpu-manager ,这个功能已经开源到https://github.com/tkestack/gpu-manager
当然也有很多其他的vgpu的方案。例如https://github.com/4paradigm/k8s-device-plugin 开源的vgpu的方案。可以直接在部署的时候就将每个卡划分为指定数量的vgpu,在占用的使用就跟原生占用方式一样,但是实际只占用2个vgpu。这种方式不会更改应用的书写方式,直接在显卡插件中进行了虚拟化。
limits:
nvidia.com/gpu: 2
此外此虚拟化方案,还可以虚化显存,用内存代替显存,存放模型。虽然这个操作对模型有一定的性能影响,但是这个操作也是有意义的,比如对于平时无访问的模型,放在虚化显存中,在访问量提升时,serverless激活服务加载到实际显存中。
Transformer
在不同的http推理架中都有对应的前置后置处理函数,例如tfserving中TensorFlow Transform,torch-server中的Custom handlers或者torchvision.transforms,tensorrt-server中的自定义backend。
例如torch-server中的Custom handlers,例如像接口用户的
data = {
"image_url":"http://xx.xx.xx/xx",
"image_id":"xx"
}
我们可以使用自定义的handler
# 请求得到响应
def handle(self, data, context):
# 输入输出为list
self.context = context
data_preprocess = self.preprocess(data)
output = self.inference(data_preprocess)
output = self.postprocess(output)
back=[]
for index in range(len(output)):
back.append({
"predict":output[index],
"image_id":data[index]['body']['image_id']
})
return back
# 前置处理
def preprocess(self, data):
des_images = []
for row in data:
image_url=row.get("body")['image_url']
image = requests.get(image_url).content
image = Image.open(io.BytesIO(image))
image = self.image_processing(image)
des_images.append(image)
return torch.stack(des_images).to(self.device)
# 后置处理
def postprocess(self, data):
return data.argmax(1).tolist()
torch-server的自定义方式是先编写handler文件,然后构建到mar文件中,这样推理服务就会使用这个hander文件。在kfserving中使用的是独立容器的方式,不影响原有业务的代码。
推理服务pipeline
服务保持无状态模式,通过前后置逻辑兼容更多的请求方式,每个服务独立控制伸缩容,监控,熔断等管理方式。链路通过用户自定义逻辑串联。
分割推理
pass
模型压缩
pass
实时训练服务
实时训练和离线训练的整体架构并不相同,在大模型实时训练中有介绍。