因为openai也用ray,所以对它还是充满好奇心的。

一、起因

团队已经用ray平台跑模型代码快2年了,该任务最近落在我身上;接触一个陌生的领域,一开始也没法就搞清楚它的原理;但是,写业务代码前我要让它在我的机器上能跑起来,能在生产环境部署运行。于是,我就见到了我的“第一个”坑(当然,它仅仅是第一个我忍不住想记一下的坑)。

二、经过

第一次碰撞

拉代码,装虚拟环境,跑测试,起服务。于是我就与问题进行了第一次碰撞,测试跑不动,既不往下运行,也不报错;尝试起服务,同样卡死;debug发现卡在了init(),issues上同样的问题,锁定grpcio==1.44.0版本,但没能直接解决我的问题。

raise ImportError(
ImportError: Failed to import grpc on Apple Silicon. On Apple Silicon machines, try `pip uninstall grpcio; conda install grpcio`. Check out https://docs.ray.io/en/master/ray-overview/installation.html#m1-mac-apple-silicon-support for more details.

参照提示信息,锁定了是M1芯片的原因;于是将virtualenv创建的虚拟环境改成了由conda创建虚拟环境,重新安装依赖,服务正常启动。

第二次碰撞

接着,开始跑测试,又迎来了与问题的第二次碰撞,测试提示pandas与numpy相关的错误,如下:

from pandas._libs.interval import Interval
pandas/_libs/interval.pyx:1: in init pandas._libs.interval
???
E   ValueError: numpy.dtype size changed, may indicate binary incompatibility. Expected 96 from C header, got 88 from PyObject
make: *** [test] Error 4

在确认业务代码需要pandas版本不能升级,选择了锁定numpy到一个低版本(参照测试环境的版本),本地测试也顺利完成了;至此,也还没来得及深入思考这是为什么,仅仅是脑海中闪过一个想法,pandas依赖numpy,但pandas没有锁定numpy版本;便开始在本地验证生产环境流程,构建ray平台的docker镜像,启动ray平台,部署模型。

小插曲

本地构建docker镜像时,遇到的一个很奇怪的问题,构建镜像失败,原因是安装依赖时,找不到ray的当前版本并提示此版本后的版本都可以安装;尽管,我也尝试进入镜像切换不同的PyPI索引,也无济于事;由于正常情况下都是在服务器上由CI/CD来自动构建发布镜像,我就先绕开了本地的这个问题,去服务器上构建镜像并且成功了。但是这个小插曲就很奇怪了,PyPI索引一样,在M1机器上安装ray成功,在服务器上构建docker镜像时安装ray成功,在M1机器上构建docker镜像时安装ray不成功。

第三次碰撞

于是,在服务器上成功的构建了docker镜像,并“成功”的启动了Ray平台;但部署模型失败了,开始了与问题的第三次碰撞;由于这一次不存在环境的差异,第一时间想到了找同事Moore帮忙,想他可能遇到过相同的问题,那就刚好直接解决了;梳理好问题的再现过程,就找到了Moore,他也是第一次遇到。于是,两人开始了一起分析问题;查看ray的状态也确实是显示成功的,看serve部署日志是提示了ray/serve代码的运行错误;带着错误信息在issues上没有关联到想要的答案。

File "/usr/local/lib/python3.10/site-packages/ray/serve/config.py", line 208, in from_proto
data = MessageToDict(
TypeError: MessageToDict() got an unexpected keyword argument 'including_default_value_fields'

因为之前部署过很多次都是OK的,所以当时两人都没有想到这个问题和ray有关,想的可能是构建镜像时执行的命令导致运行环境的变化,结果在排除这个问题后,我联想到之前的grpcio、numpy版本问题,难道是ray没有锁定第三方依赖,一看果然如此;此时我的想法是将ray所有没有固定的依赖在全部在构建镜像时固定,做一个基础镜像,以后就可以避免同类型的问题了,此刻觉着Node.js侧的yarn.lock还是挺好的。Moore也很快定位到具体是哪个包升级导致的。 site-packages/google/protobuf/json_format.py 的 MessageToDict 方法升级到5.x.x版本就没有including_default_value_fields参数了。

三、思考

pip能否像npm一样锁定依赖版本?

可以,只不过node侧是安装依赖时,自动生成或更新相应的lock文件,来锁定版本加速安装。pip 需要手动来锁定已安装依赖的版本。

# 锁定依赖
pip freeze > requirements.txt

# 安装依赖
pip install -r requirements.txt

当然,也有第三方工具像pip-tools、poetry等。

为什么第三方库不完全指定其第三方库在小版本范围内变化?

更多的是考虑兼容性,宽松的版本范围可以减少与其他库之间的依赖冲突。与Node.js不同,Node.js 使用 npm 或 yarn 或 pnpm 作为包管理器,它们具有处理依赖版本冲突的机制。具体来说,Node.js 的模块系统允许不同的包在各自的 node_modules 目录下拥有自己版本的依赖。 举例:

project/
├── node_modules/
│   ├── packageA/
│   │   └── node_modules/
│   │       └── commonDependency@1.0.0
│   └── packageB/
│       └── node_modules/
│           └── commonDependency@2.0.0
├── package.json

Python 使用 pip 作为包管理器。在同一个虚拟环境或项目中,无法安装两个不同版本的同一个依赖,因为 Python 的依赖导入机制不支持这样做,所以宽松的版本对于与其他依赖兼容性有显著的提升。 当然,在开发项目时,作为非第三库,锁定当前项目的依赖版本及全覆盖的单元测试就显得尤为重要了。

四、结果

因为在运行的过程中,遇到了上述的问题,决定对于开发的模型库的依赖选择设置宽松的版本,通过全覆盖的单元测试确保其健壮性;对于模型服务则选择锁定版本来确保每次构建的稳定性。 对于过程中的小插曲,由于正常情况下不需要在开发机器上构建镜像所以暂时不解决;当然,也可以先在服务器上构建一个已安装ray的基础镜像,基于此基础镜像进行业务代码层面的构建。

五、Ray

Ray 是一个功能强大且灵活的分布式计算框架,适用于各种大规模计算任务。它简化了分布式计算的开发和管理,使开发者能够专注于业务逻辑而不是底层的计算复杂性。

ray