平台架构 - 推理服务
说明:istio 分流与网关入口适配与"平台架构 - 流量代理与网关"有重叠;prometheus 服务发现 / hpa 与"平台架构 - 监控体系"有重叠;vgpu 与 平台架构-分布式训练与加速.md 有重叠。流量代理与监控的更系统介绍参见 01-架构原理/ 中对应文档及 04-运维管理/。
推理服务各阶段
推理服务从底层到上层,包含服务网格、serverless、pipeline、http 框架、模型计算。
- 服务网格阶段:主要工作是代理流量的中转和管控,例如分流、镜像、限流、黑白名单之类的。
- serverless 阶段:主要为服务的智能化运维,例如服务的激活、伸缩容、版本管理、蓝绿发布。
- pipeline 阶段:主要为请求在各数据处理/推理之间的流动,推理的前后置处理逻辑等。
- http/grpc 框架:主要为处理客户端的请求,准备推理样本,推理后作出响应。
- 模型计算:模型在 cpu/gpu 上对输入样本做前向计算。
网关入口适配
全部改为 istio ingressgateway 的网关方案。
CubeStudio 中的很多功能是通过外部镜像实现的,比如 jupyter、vscode、nni、k8s-dashboard、kfp、云原生服务用户自己定义的镜像,以及推理服务外部各框架镜像、kfserving 镜像等。它们在网关入口处基本通过 url prefix 或者 host 进行区分代理,内部再加上泛化域名的能力,这样就能部署后直接访问了。
分流 + 流量复制
分流表示将一批流量按照一定规则分发到多个服务上去,每一个请求只会进入一个服务端;流量镜像则表示将原有的流量原封不动地复制一份到另一个服务。这两种功能在实际生产中都非常有用。分流比较好理解,流量复制如下图所示,发起端是不等待镜像服务的响应的。

在 istio 中是通过虚拟服务(VirtualService)和目标规则(DestinationRule)来管理流量的策略的,它们与 k8s 的关系如下图。

在 istio 中可以很方便地实现流量的分发和复制。CubeStudio 里面一个模型使用一个唯一的 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
说明:上述示例中的命名空间
kfserving为早期 kfserving 方案的写法。当前代码中推理服务统一部署在命名空间service(SERVICE_NAMESPACE = 'service',见myapp/config.py:776;SERVICE_PIPELINE_NAMESPACE='service'见myapp/config.py:770),实际 host 后缀应为.service.svc.cluster.local,示例仅作流量复制语法演示。
在很多情况下,我们需要将真实的流量数据与镜像流量数据进行收集并分析,需要在其中将各个复制端的请求和结果导入到对比系统中进行比较,查看服务的好坏。
在 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
prometheus 服务发现
在 k8s 中由于服务 pod 的 ip 是变化的,所以不能像在主机上一样固定写死 pod 的 ip,需要服务的自动发现。在原有方案中需要为服务注册 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 的端点:
# annotation 是键值结构,源标签设为键、regex 设为值,当值匹配 regex 时执行 keep 动作保留,其余丢弃。
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape]
action: keep
regex: true
# 匹配 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" 定义的 metrics 暴露地址,
# 若不是默认的 /metrics,则赋值给 __metrics_path__ 变量,prometheus 据此拼接完整 URL 抓取。
- 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", namespace="xx", 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 为 pod、工作 pod 所在命名空间为 namespace,通过这两个就可以匹配到目标 pod。
ℹ️ 核实:指标名
DCGM_FI_DEV_GPU_UTIL在平台侧 prometheus 查询中确实使用,见myapp/utils/py/py_prometheus.py:104/202/333/458;自定义伸缩指标命名为container_gpu_usage,见myapp/utils/py/py_k8s.py:2246。
我们通过 configmap 的形式注册 apiserver 的指标:
# gpu 利用率
- seriesQuery: '{__name__="DCGM_FI_DEV_GPU_UTIL",pod!="",namespace="service"}'
seriesFilters: []
# pod 自定义指标供 hpa 查询指定命名空间指定 pod 的指标,
# 需通过 resources 配置指标标签和命名空间、pod 名称的映射关系
resources:
overrides:
namespace: # 指标的标签
resource: namespace # k8s 的资源名
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 namespace 为真实 pod 的 namespace、指标中的 pod 为真实 pod 的 pod 名(推理服务命名空间为 service)。并且在实际推理中请求进来 gpu 利用率才会变高,所以为了避免太过敏感,最终用于 hpa 的指标值做了 5 分钟平均。
各类推理框架
不同的训练框架基本都有对应的推理框架。CubeStudio 为 tf/torch/onnx/tensorrt/lightgbm/paddle/sklearn/xgboost 等模型提供推理服务,基本仅需用户填写模型名称、版本、模型地址,会自动生成所有的配置文件、提供 api 的 demo、自动提供域名和 ip 的访问。
各类型的推理框架或多或少可以代理其他训练框架的模型进行推理服务。例如 tfserving 也可以为 onnx 模型提供推理服务,所以这些框架和模型并不是唯一绑定关系。
ℹ️ 核实(当前框架枚举已扩展):当前代码中推理服务类型
service_type已扩展为ml-server(sklearn/xgboost/lightgbm 等通用 ml)、tfserving、torch-server、triton-server(即下文的 tensorrt/Triton)、vllm、vllm-distributed、ollama、mindie、mindie-distributed、triton-llm等,见myapp/views/view_inferenceserving.py:58-94(含各框架的模型路径、健康检查 host、configmap、启动命令)。下文的 onnxruntime/tensorrt 描述对应当前的triton-server。
tfserving
tfserving 主要是 tf 模型推理服务,虽然同时也可以为其他模型提供推理服务(比如上面说的 onnx 模型)。
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 监控。
ℹ️ 核实:tfserving 启动命令为
/usr/bin/tf_serving_entrypoint.sh --model_config_file=/config/models.config --monitoring_config_file=/config/monitoring.config --platform_config_file=/config/platform.config ...,健康检查 host 为/v1/models/$model_name/metadata,见myapp/views/view_inferenceserving.py:68,87。
torchserve(torch-server)
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 的监控。
ℹ️ 核实:torch-server 的 config.properties 中
inference_address=http://0.0.0.0:8080、management_address=http://0.0.0.0:8081、metrics_address=http://0.0.0.0:8082,健康检查 host 为:8081/models,见myapp/views/view_inferenceserving.py:69,81。
onnxruntime
pass(当前由 triton-server 统一承载 onnx/tensorrt 等模型,见上文框架枚举核实)
模型压缩
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,请求的方式不一样,需要先根据实际情况判断组合该用什么样的请求流。
ℹ️ 核实:当前平台中 tensorrt/Triton 对应
triton-server,启动命令tritonserver --model-repository=/models/ --strict-model-config=true --log-verbose=1,健康检查 host 为/v2/models/$model_name,见myapp/views/view_inferenceserving.py:70,89。
vgpu
gpu 推理时三类场景需要用到 gpu:
- gpu 显存和 gpu 利用率低:vgpu 可以在一张 gpu 卡上放更多的推理服务,共用一个 gpu。
- 本身 gpu 数量有限,需要更多的 gpu 数量满足需求:vgpu 可以虚化出更多的 gpu 数量。
- 显存不充足、模型比较大,或大模型却没有较多的 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" # 写相对卡顺序,比较方便
ℹ️ 核实:CubeStudio 在训练/任务侧也是通过环境变量
NVIDIA_VISIBLE_DEVICES指定卡号(同时注入ASCEND_RT_VISIBLE_DEVICES兼容昇腾),见myapp/views/view_pipeline.py:724-726、myapp/views/view_nni.py:468-470。
如果应用在 tke 上,也可以直接部署 qgpu 组件,使用 vgpu 的功能。在 tke 上可以占用卡的一部分,显存为 vcuda-memory*256Mi,独立占用 gpu 核和显存两个资源:
limits:
tencent.com/vcuda-core: 200
tencent.com/vcuda-memory: 60
如果是在自建 k8s 上,也可以使用 HAMi,这个功能已经开源到 https://github.com/Project-HAMi/HAMi 。
当然也有很多其他的 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 文件中,这样推理服务就会使用这个 handler 文件。在 kfserving 中使用的是独立容器的方式,不影响原有业务的代码。

推理服务 pipeline
服务保持无状态模式,通过前后置逻辑兼容更多的请求方式,每个服务独立控制伸缩容、监控、熔断等管理方式,链路通过用户自定义逻辑串联。
