关于程序员:用-Go-跑的更快使用-Golang-为机器学习服务

30次阅读

共计 4388 个字符,预计需要花费 11 分钟才能阅读完成。

用 Go 跑的更快:应用 Golang 为机器学习服务

因而,咱们的要求是用尽可能少的资源实现每秒 300 万次的预测。值得庆幸的是,这是一种比较简单的举荐零碎模型,即多臂老虎机(MAB)。多臂老虎机通常波及从 Beta 散布 等散布中取样。这也是破费工夫最多的中央。如果咱们能同时做尽可能多的采样,咱们就能很好地利用资源。最大限度地进步资源利用率是缩小模型所需总体资源的要害。

咱们目前的预测服务是用 Python 编写的微服务,它们遵循以下个别构造:

申请 -> 性能获取 -> 预测 -> 前期解决 -> 返回

一个申请可能须要咱们对成千上万的用户、内容对进行评分。带有 GIL 和多过程的 Python 解决性能很鸡肋,咱们曾经实现了基于 cython 和 C++ 的批量采样办法,绕过了 GIL,咱们应用了许多基于内核数量的 workers 来并发解决申请。

目前单节点的 Python 服务能够做 192 个 RPS,每个大概 400 对。均匀 CPU 利用率只有 20% 左右。当初的限度因素是语言、服务框架和对存储性能的网络调用。

为什么是 Golang?

Golang 是一种动态类型的语言,具备很好的工具性。这意味着谬误会被及早发现,而且很容易重构代码。Golang 的并发性是原生的,这对于能够并行运行的机器学习算法和对 Featurestore 的并发网络调用十分重要。它是 这里 基准最快的服务语言之一。它也是一种编译语言,所以它在编译时能够进行很好的优化。

移植现有的 MAB 到 Golang 上

基本思路,将零碎分为 3 个局部:

  • 用于预测和衰弱的根本 REST API 与存根
  • Featurestore 的获取,为此实现一个模块
  • 应用 cgo 晋升和转移 c++ 的采样代码

第一局部很容易,我抉择了 Fiber 框架用于 REST API。它仿佛是最受欢迎的,有很好的文档,相似 Expressjs 的 API。而且它在基准测试中的体现也相当杰出。

晚期代码:

func main() {
    // setup fiber
    app := fiber.New()
    // catch all exception
    app.Use(recover.New())
    // load model struct
    ctx := context.Background()
    md, err := model.NewModel(ctx)
    if err != nil {fmt.Println(err)
    }
    defer md.Close()

    // health API
    app.Get("/health", func(c *fiber.Ctx) error {
        if err != nil {
            return fiber.NewError(
                fiber.StatusServiceUnavailable, 
                fmt.Sprintf("Model couldn't load: %v", err))
        }
        return c.JSON(&fiber.Map{"status": "ok",})
    })
    // predict API
    app.Post("/predict", func(c *fiber.Ctx) error {var request map[string]interface{}
        err := json.Unmarshal(c.Body(), &request)
        if err != nil {return err}

        return c.JSON(md.Predict(request))
    })

就这样,工作一实现了。花了不到一个小时。

在第二局部中,须要略微学习一下如何编写 带办法的构造 和 goroutines。与 C++ 和 Python 的次要区别之一是,Golang 不反对齐全的面向对象编程,次要是不反对继承。它在构造体上的办法的定义形式也与我遇到的其余语言齐全不同。

咱们应用的 Featurestore 有 Golang 客户端,我所要做的就是在它四周写一个封装器来读取大量并发的实体。

我想要的根本构造是:

type VertexFeatureStoreClient struct {//client reference to gcp's client}

func NewVertexFeatureStoreClient(ctx context.Context,) (*VertexFeatureStoreClient, error) {// client creation code}

func (vfs *VertexFeatureStoreClient) GetFeaturesByIdsChunk(ctx context.Context, featurestore, entityName string, entityIds []string, featureList []string) (map[string]map[string]interface{}, error) {// fetch code for 100 items}

func (vfs *VertexFeatureStoreClient) GetFeaturesByIds(ctx context.Context, featurestore, entityName string, entityIds []string, featureList []string) (map[string]map[string]interface{}, error) {
    const chunkSize = 100 // limit from GCP
    // code to run each fetch concurrently
    featureChannel := make(chan map[string]map[string]interface{})
    errorChannel := make(chan error)
    var count = 0
    for i := 0; i < len(entityIds); i += chunkSize {
        end := i + chunkSize
        if end > len(entityIds) {end = len(entityIds)
        }
        go func(ents []string) {features, err := vfs.GetFeaturesByIdsChunk(ctx, featurestore, entityName, ents, featureList)
            if err != nil {
                errorChannel <- err
                return
            }
            featureChannel <- features
        }(entityIds[i:end])
        count++
    }
    results := make(map[string]map[string]interface{}, len(entityIds))
    for {
        select {
        case err := <-errorChannel:
            return nil, err
        case res := <-featureChannel:
            for k, v := range res {results[k] = v
            }
        }
        count--
        if count < 1 {break}
    }

    return results, nil
}
func (vfs *VertexFeatureStoreClient) Close() error {//close code}
对于 Goroutine 的提醒

尽量多应用通道,有很多教程应用 Goroutine 的 sync workgroups。那些是较低级别的 API,在大多数状况下你都不须要。通道是运行 Goroutine 的优雅形式,即便你不须要传递数据,你能够在通道中发送标记来收集。goroutines 是便宜的虚构线程,你不用放心制作太多的线程并在多个外围上运行。最新的 golang 能够为你跨外围运行。

对于第三局部,这是最难的局部。花了大概一天的工夫来调试它。所以,如果你的用例不须要简单的采样和 C++,我倡议间接应用 Gonum,你会为本人节俭很多工夫。

我没有意识到,从 cython 来的时候,我必须手动编译 C++ 文件,并将其加载到 cgo include flags 中。

头文件:

#ifndef BETA_DIST_H
#define BETA_DIST_H

#ifdef __cplusplus
extern "C"
{
#endif

    double beta_sample(double, double, long);
#ifdef __cplusplus
}
#endif

#endif

留神 extern C,这是 C++ 代码在 go 中应用的须要,因为 mangling,C 不须要。另一个问题是,我不能在头文件中做任何 #include 语句,在这种状况下 cgo 链接失败(起因不明)。所以我把这些语句移到 .cpp 文件中。

编译它:

g++ -fPIC -I/usr/local/include -L/usr/local/lib  betadist.cpp -shared -o libbetadist.so

一旦编译实现,你就能够应用它的 cgo。

cgo 包装文件:

/*
#cgo CPPFLAGS: -I${SRCDIR}/cbetadist
#cgo CPPFLAGS: -I/usr/local/include
#cgo LDFLAGS: -Wl,-rpath,${SRCDIR}/cbetadist
#cgo LDFLAGS: -L${SRCDIR}/cbetadist
#cgo LDFLAGS: -L/usr/local/lib
#cgo LDFLAGS: -lstdc++
#cgo LDFLAGS: -lbetadist
#include <betadist.hpp>
*/
import "C"

func Betasample(alpha, beta float64, random int) float64 {return float64(C.beta_sample(C.double(alpha), C.double(beta), C.long(random)))
}

留神 LDFLAGS 中的 -lbetadist 是用来链接 libbetadist.so 的。你还必须运行 export DYLD_LIBRARY_PATH=/fullpath_to/folder_containing_so_file/。而后我能够运行 go run .,它可能像 go 我的项目一样工作。

将它们与简略的模型构造和预测办法整合在一起是很简略的,而且相对来说破费的工夫更少。

后果

Metric Python Go
Max RPS 192 819
Max latency 78ms 110ms
Max CPU util. ~20% ~55%

这是对 RPS 的 4.3 倍 的晋升,这使咱们的最低节点数量从 80 个缩小到 19 个,这是一个微小的老本劣势。最大提早略高,但这是能够承受的,因为 python 服务在 192 点时就曾经饱和了,如果流量超过这个数字,就会显著降落。

我应该把我所有的模型转换为 Golang 吗?

简短的答案:不必。

长答案。Go 在服务方面有很大的劣势,但 Python 依然是试验的王道。我只倡议在模型简略且长期运行的根底模型中应用 Go,而不是试验。Go 对于简单的 ML 用例来说 尚 不是很成熟。

所以房间里的大象,为什么不是 Rust?

嗯,希夫做到了。看看吧。它甚至比 Go 还快。

本文由 mdnice 多平台公布

正文完
 0