用 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 __cplusplusextern "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多平台公布