c
如果你问的是 **Ray(Python 分布式计算框架)里的代码怎么发到 worker 节点执行**,可以分成两层理解:
## 1. `@ray.remote` 函数/Actor 本身怎么分发?
典型代码:
```python
import ray
ray.init(address="auto")
@ray.remote
def f(x):
return x + 1
ref = f.remote(1)
print(ray.get(ref))
Ray 的大致流程是:
- Driver 进程里定义
@ray.remote函数或 Actor。 - 第一次调用
.remote()时,Ray 会把这个函数/类序列化。 - Ray 把序列化后的函数/类定义放到集群的 GCS / internal KV 里。
- 被调度到某个节点上的 worker 通过函数描述符拿到这段序列化代码。
- worker 反序列化后执行任务。
Ray 官方的 task lifecycle 文档里说明,RemoteFunction 会把底层函数 pickle 成字节,并存到 GCS key-value store,远端 executor 再取出、反序列化并执行。[1](#1)
Ray 的序列化基于 Pickle protocol 5,并借助 cloudpickle 支持 lambda、嵌套函数、动态类等 Python 对象。[2](#2)
2. 你的项目代码、依赖包怎么分发?
这是更容易踩坑的地方。
Ray 不会默认把你本地整个项目目录自动同步到所有节点 。@ray.remote 函数本身可以被序列化,但它里面 import 的模块、配置文件、模型文件、依赖库等,worker 节点也必须能访问。
通常有几种方式:
方式 A:用 runtime_env={"working_dir": ...} 分发项目目录
例如:
python
ray.init(
address="auto",
runtime_env={
"working_dir": ".",
"pip": ["numpy", "pandas"]
}
)
含义是:
- 把当前目录作为运行目录分发到 worker;
- worker 执行 task/actor 时可以 import 这个目录里的代码;
pip字段用于安装 Python 依赖。
Ray 官方文档里 working_dir 被描述为本地路径或远程 URI,Ray 会把它解包到每个 task/actor 的工作目录中。[3](#3)
方式 B:用 runtime_env={"py_modules": [...]} 分发 Python 模块
例如:
python
ray.init(
address="auto",
runtime_env={
"py_modules": ["./my_package"]
}
)
适合分发某个 Python package。Ray 会把这些模块加入 worker 的 PYTHONPATH。官方 API 文档说明,py_modules 可以是本地路径或远程 URI,Ray 会解包并插入 worker 的 PYTHONPATH。[3](#3)
方式 C:把代码放到远程 URI
例如:
python
ray.init(
address="auto",
runtime_env={
"working_dir": "s3://my-bucket/my_project.zip"
}
)
或者:
yaml
runtime_env:
working_dir: "https://github.com/xxx/project/archive/HEAD.zip"
这种方式更适合生产环境,因为不依赖 driver 本地机器上传代码。Ray Serve 文档也建议生产环境可以把代码打进镜像,或者用 runtime_env 指向远程存储中的代码。[4](#4)
方式 D:直接把代码和依赖打进镜像
生产环境最稳的方式通常是:
Dockerfile
FROM rayproject/ray:latest
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
然后整个 Ray 集群都用这个镜像启动。
优点:
- 启动更稳定;
- 不需要每次 job 提交都上传代码;
- 依赖版本一致;
- 更适合 Kubernetes / KubeRay / 生产部署。
3. 参数和返回值怎么分发?
函数代码是一回事,任务参数和返回值是另一回事。
例如:
python
@ray.remote
def g(arr):
return arr.sum()
调用:
python
ref = g.remote(big_array)
Ray 会把参数序列化后放入对象存储。Ray 文档说明,Ray 使用 Plasma object store 在进程和节点之间高效传输对象;每个节点有自己的 object store,对象不会自动广播到所有节点,而是在其他 task/actor 请求时才传输。[2](#2)
所以:
- 小参数会随 task 提交;
- 大对象通常进入 Ray object store;
- 跨节点需要时才拉取;
- 返回值也是通过
ObjectRef引用。
4. 一个直观总结
可以理解成:
text
Driver
|
| 1. 序列化 @ray.remote 函数/Actor
v
GCS / internal KV
|
| 2. worker 拉取函数定义
v
Worker process
同时:
项目目录 / Python 模块 / pip 依赖
|
| 通过 runtime_env 或镜像分发
v
Worker runtime environment
任务参数 / 返回值
|
| 通过 Ray object store 分发
v
ObjectRef / Plasma store
5. 常见坑
坑 1:只定义了 remote 函数,但 worker 找不到本地模块
比如:
python
from my_project.utils import helper
@ray.remote
def f(x):
return helper(x)
如果 worker 节点没有 my_project,会报 ModuleNotFoundError。
解决:
python
ray.init(
address="auto",
runtime_env={"working_dir": "."}
)
或者把 my_project 安装成包:
python
ray.init(
address="auto",
runtime_env={"pip": ["my_project==0.1.0"]}
)
坑 2:把大对象放进闭包
不要这样:
python
big_model = load_model()
@ray.remote
def infer(x):
return big_model(x)
这样 big_model 可能会跟函数一起被序列化,造成巨大开销。
更好的方式:
python
model_ref = ray.put(load_model())
@ray.remote
def infer(model, x):
return model(x)
ref = infer.remote(model_ref, data)
或者用 Actor 持有模型:
python
@ray.remote
class ModelWorker:
def __init__(self):
self.model = load_model()
def infer(self, x):
return self.model(x)
坑 3:开发环境和生产环境混用
开发时可以用:
python
runtime_env={"working_dir": "."}
生产时更推荐:
text
Docker image + fixed requirements.txt / lockfile
这样可复现性更好。
简短结论
Ray 的代码分发可以概括为:
@ray.remote函数/Actor:Ray 用 pickle/cloudpickle 序列化后放到 GCS,worker 拉取后执行。- 项目源码:不会默认全量同步,需要用
runtime_env["working_dir"]、runtime_env["py_modules"]、远程 zip URI,或打进 Docker 镜像。 - Python 依赖:通过
runtime_env["pip"]、conda、uv,或镜像安装。 - 参数和返回值:通过 Ray object store /
ObjectRef分发,不是代码分发。