使用函数 initializer 接口优化深度学习场景下模型加载的冷启动延时

背景

深度学习场景使用函数计算典型案例

阿里云 函数计算 客户 码隆科技 是一家专注于深度学习与计算机视觉技术创新的公司。当码隆的客户上传大量图像数据后,需要尽快把图像按照客户指定的方式处理,包括商品识别,纺织面料等柔性材质识别分析,内容审查,以图搜图等等。图像处理基于码隆预先训练好的深度学习模型,要求在短时间内准备大量的计算资源进行大规模并行处理。客户将深度学习推理逻辑实现为函数,在函数中加载模型后对图像数据进行处理。通过函数计算提供的大规模计算能力,客户能够短时间处理大量图像,平稳应对峰值压力。更多详细案例请见 函数计算客户案例

深度学习场景的客户在使用函数计算服务中更希望平台做哪些改进?

深度学习场景下加载模型是主要的应用层冷启动开销,模型的规格多为 500MB+,应用层冷启动开销往往会导致毛刺的产生,为归避这类问题,函数计算引入了 initializer 接口来解决应用层冷启动开销带来的毛刺问题。

功能简介

Initializer 编程模式为用户提供了 initializer 入口定义,便于用户将业务逻辑分为initializer函数请求处理函数两部分。函数计算使用容器执行用户函数代码,这样的执行环境我们称之为函数实例。函数实例会在启动的时候能够自动执行 initializer 函数,进行业务层冷启动,成功之后,该实例收到用户的 Invoke 请求,就能够执行用户的请求处理函数了。

引入 initializer 接口的优势:

  • 分离初始化逻辑和请求处理逻辑,程序逻辑更清晰,让用户更易写出结构良好,性能更优的代码;
  • 用户函数代码更新时,系统能够保证用户函数的平滑升级,规避应用层初始化冷启动带来的性能损耗。新的函数实例启动后能够自动执行用户的初始化逻辑,在初始化完成后再处理请求;
  • 在应用负载上升,需要增加更多函数实例时,系统能够识别函数应用层初始化的开销,更精准的计算资源伸缩的时机和所需的资源量,让请求延时更加平稳;
  • 即使在用户有持续的请求且不更新函数的情况下,FC系统仍然有可能将已有容器回收或更新,这时没有平台方(FC)的冷启动,但是会有业务方冷启动,Initializer 的引入可以最大限度减少这种情况;

案例实践

本实践以 函数计算部署机器学习遇到的问题和解法 这篇文章为基础,做了进一步改造和优化。下文将按照以下几个步骤讲解如何利用函数计算以高性能、低延时玩转深度学习场景下的识别手写数字案例:

安装依赖

训练模型

首先需要训练预期的模型,模型的训练可参考 这篇文章。按照文章中的步骤下载 MINIST 数据库和相关代码并开始训练模型,训练时长持续半小时左右,训练成功后的结构目录如下,其中 model_data 目录下的文件便是通过训练得到的模型。

project root
├── main.py
├── grf.pb
└── model_data
    ├── checkpoint
    ├── model.data-00000-of-00001
    ├── model.index
    └── model.meta

应用依赖的安装

本案例需要安装的应用依赖有 tensorflowopencv-python,模型的训练和函数的处理逻辑都强依赖这两个库,训练模型可在本地直接操作,通过 pip 在本地安装两个依赖库即可,版本不限。由于函数运行在函数计算(FC)系统同样依赖这两个库,需要提前下载好依赖并打包上传到 OSS。推荐使用 fcli 工具的 sbox 命令,下面以 runtime 为 python2.7 进行操作:

目前 Pypi 上 tensorflow 最新版本为 1.11.0,为避免因版本问题影响您的实践,建议安装 1.8.0 版本。

cd <'此项目的根目录中'>
mkdir applib      // 创建存储所有应用依赖的目录
fcli shell        // fcli version >= 0.24
sbox -d applib -t python2.7
pip install -t $(pwd) tensorflow==1.8.0
pip install -t $(pwd) opencv-python

完成之后 exit 退出沙盒环境,并执行 exit 退出fcli。

上传依赖

依赖和模型下载成功后需要进行压缩并上传到 OSS 中,以便后面函数可以直接从 OSS 下载即可,在项目的根目录执行下面两条命令可以得到 applib.zipmodel_data.zip 两个 zip 压缩包。

cd applib && zip -r applib.zip * && mv applib.zip ../ ; cd ..
cd model_data && zip -r model_data.zip * && mv model_data.zip ../ ; cd ..

下面提供了一个简单的上传 zip 包到 OSS 的模版,上传成功后删除本地的依赖和模型目录即可。

# -*- coding: utf-8 -*-
import oss2

auth = oss2.Auth(<'Your access_key_id'>, <'Your access_key_secret'>)
bucket = oss2.Bucket(auth, <'Your endpoint'>, <'Your bucket'>)
bucket.put_object_from_file('applib.zip', <'Your applib.zip path'>)
bucket.put_object_from_file('model_data.zip', <'Your model_data.zip path'>)

将机器学习应用迁移至函数计算

如何将本地机器学习应用进行改造并迁移到函数计算的流程在 将机器学习应用迁移至函数计算 中有详细的步骤,这篇文章中的改造并没有 initializer 的概念,您只需要关注 index.py 和 loader.py 是如何产生的,详细代码链接,改造后的目录结构如下,其中 index.py 存放了机器学习相关逻辑的代码,loader.py 存放了函数入口和加载依赖逻辑的代码。

project root
└─── code
    ├── loader.py
    └── index.py
    └── pic
        └── e2.jpg

e2.jpg 只是文章中提供的一个简单的数字 2 图片,如做验证性测试需要更多的素材可以通过 keras.js 平台手动绘制生成。

使用函数 initializer 接口优化深度学习场景下模型加载的冷启动延时

引入 initializer 接口

经过应用迁移处理后得到了一个可以运行在函数计算服务上的函数,很明显可以看到函数入口 loader.handler 中首先需要从 OSS 加载应用依赖(tensorflow、opencv)和资源依赖(模型),加载的过程都属于应用层冷启动,冷启动所耗费的时间在一定程度上和所需依赖的大小规格成正比。为避免后续处理逻辑受到应用层冷启动延时的影响,这里将加载依赖逻辑放入 initializer 函数中。

其中 index.py 文件保持不变,loader.py 文件需要进行如下改造:

  • 添加 initializer 函数,initializer 入口便为 loader.initializer。
  • 将对 download_and_unzip_if_not_exist 的调用从 handler 中更换到 initializer 函数中。

loader.py 经过改造后的代码如下:

# -*- coding:utf-8 -*-
import sys
import zipfile
import os
import oss2
import imp
import time

app_lib_object = os.environ['AppLibObject']
app_lib_dir = os.environ['AppLibDir']
model_object = os.environ['ModelObject']
model_dir = os.environ['ModelDir']

local = bool(os.getenv('local', ""))
print 'local running: ' + str(local)

def download_and_unzip_if_not_exist(objectKey, path, context):
    creds = context.credentials
    if (local):
        print 'thank you for running function in local!!!!!'
        auth = oss2.Auth(creds.access_key_id,
                         creds.access_key_secret)
    else:
        auth = oss2.StsAuth(creds.access_key_id,
                            creds.access_key_secret,
                            creds.security_token)

    endpoint = os.environ['Endpoint']
    bucket = os.environ['Bucket']

    print 'objectKey: ' + objectKey
    print 'path: ' + path
    print 'endpoint: ' + endpoint
    print 'bucket: ' + bucket

    bucket = oss2.Bucket(auth, endpoint, bucket)

    zipName = '/tmp/tmp.zip'

    print 'before downloading ' + objectKey + ' ...'
    start_download_time = time.time()
    bucket.get_object_to_file(objectKey, zipName)
    print 'after downloading, used %s seconds...' % (time.time() - start_download_time)

    if not os.path.exists(path):
        os.mkdir(path)

    print 'before unzipping ' + objectKey + ' ...'
    start_unzip_time = time.time()
    with zipfile.ZipFile(zipName, "r") as z:
        z.extractall(path)
    print 'unzipping done, used %s seconds...' % (time.time() - start_unzip_time)

def initializer(context):
    if not local:
        download_and_unzip_if_not_exist(app_lib_object, app_lib_dir, context)
        download_and_unzip_if_not_exist(model_object, model_dir, context)
    sys.path.insert(1, app_lib_dir)

def handler(event, context):
    desc = None
    fn, modulePath, desc = imp.find_module('index')
    mod = imp.load_module('index', fn, modulePath, desc)
    request_handler = getattr(mod, 'handler')
    return request_handler(event, context)

部署

本地开发已经完成,下面借助阿里云函数计算的工具 fun 可以进行一键部署,Fun 是一个用于支持 Serverless 应用部署的工具,它通过一个资源配置文件(template.yml),协助您进行开发、构建、部署操作,步骤如下:

  • release 页面对应平台的 binary 版本,解压就可以使用。或者使用 npm install @alicloud/fun * -g 也可以直接使用。
  • 使用 fun config 配置 ak、region 等信息。
  • 编写 template.yml
  • fun deploy 部署

template.yml 文件如下:

ROSTemplateFormatVersion: '2015-09-01'
Transform: 'Aliyun::Serverless-2018-04-03'
Resources:
  tensorflow: # 服务名
    Type: 'Aliyun::Serverless::Service'
    Properties:
      Description: 'tensorflow demo'
      Policies:
        - AliyunOSSReadOnlyAccess
    initializer: # 函数名
      Type: 'Aliyun::Serverless::Function'
      Properties:
        Handler: loader.handler  # 处理函数入口
        Initializer: loader.initializer  # initializer 入口
        CodeUri: ./code/
        Description: 'tensorflow application!'
        Runtime: python2.7
        MemorySize: 1024
        Timeout: 300
        InitializationTimeout: 60
        EnvironmentVariables:
          Bucket: test-bucket # 替换为自己的 oss bucket
          Endpoint: 'https://oss-cn-hangzhou.aliyuncs.com' # 替换掉 OSS Endpoint
          AppLibObject: applib.zip
          AppLibDir: /tmp/applib
          ModelObject: model_data.zip
          ModelDir: /tmp/model
  • 执行 fun deploy 会显示如下信息,部署成功后可到对应 region 下查看部署是否生效。
    使用函数 initializer 接口优化深度学习场景下模型加载的冷启动延时

测试

功能测试

登陆函数计算 控制台 对应 region 下找到所创建的函数,连续执行两次查看执行结果如下。

  • 首次执行
    使用函数 initializer 接口优化深度学习场景下模型加载的冷启动延时
  • 第二次执行
    使用函数 initializer 接口优化深度学习场景下模型加载的冷启动延时

从以上图片可以看到首次函数执行时间为 3793ms,第二次函数执行时间为 810ms,执行结果都为 the predict is 2 ,从执行结果可以确认函数执行正确,但性能真的提高了吗?下面会有简单的性能测试做对比。

性能测试

这里对改造前的函数做同样的测试:

  • 首次执行
    使用函数 initializer 接口优化深度学习场景下模型加载的冷启动延时
  • 首次执行日志
    使用函数 initializer 接口优化深度学习场景下模型加载的冷启动延时
  • 第二次执行
    使用函数 initializer 接口优化深度学习场景下模型加载的冷启动延时

从以上图片可以看到首次函数执行时间为 17506ms,第二次函数执行时间为 815ms,通过日志可以发现首次触发函数执行大约 13s 花费在加载模型和依赖库上,函数的执行时间会随着模型和依赖库规格的增大而增大。由此可见,initializer 函数的引入会使得函数实例在首次启动时规避冷启动开销,降低函数执行时间,提高函数性能,并且不会对后续的请求产生任何影响。

总结

通过将深度学习场景下规格较大的模型、依赖库的加载等初始化逻辑进行提取放到 initializer 函数中可以极大的提升函数性能,规避用户系统/函数升级带来的冷启动开销,帮助用户实现业务系统的热升级。

最后欢迎大家通过扫码加入我们用户群中,使用过程中有问题或者有其他问题可以在群里提出来。函数计算官网客户群(11721331)。
使用函数 initializer 接口优化深度学习场景下模型加载的冷启动延时

参考文章:

1 :Tensorflow MINIST数据模型的训练,保存,恢复和手写字体识别
2:函数计算部署机器学习遇到的问题和解法

上一篇:使用JavaScript的FormData向SAP ABAP系统发起登录请求


下一篇:PostgreSQL 11 preview - compress method 接口 - 暨开放接口系列