Ray远程调试从原理到实现

虽然讲清楚Ray远程调试的步骤很容易,但是希望能做到知其所以然,因此本文首先从VSCode的调试原理讲起。

VSCode是如何调试Python程序的

VSCode的调试架构是一个客户端-服务器模型,其客户端是VSCode,提供调试界面、发送调试命令。服务端则是调试器debugpy,管理 Python 调试会话,如断点注入、执行控制等。其中的通信协议是调试适配器协议( Debug Adapter Protocol, DAP)。这里就要重点介绍一下debugpy了。debugpy支持两种模式,launch和attach。

launch模式

launch模式就是我们一般使用的方式,即由IDE启动程序,此时debugpy 被自动注入,通过内部机制(如本地进程间通信 IPC) 与 Python 程序通信。launch模式适合本地开发调试。

deepseek_mermaid_20250605_848ef2.png

此时你的lauch.json大概是这个样子:

1
2
3
4
5
6
7
{  
    "name": "Python: Current File",  
    "type": "python",  
    "request": "launch",  // 使用 launch 模式  
    "program": "${file}", // 调试当前文件  
    "console": "integratedTerminal"  
}  

attach模式

attach模式则是将调试器附加到一个已经运行的 Python 进程上进行调试的方法。通常情况下,我们在终端启动程序而不是用IDE,而debugpy通过在指定端口(默认是5678)启动一个本地 TCP 服务来监听客户端的连接,从而实现调试功能。

deepseek_mermaid_20250605_361af0.png

启动attach模式,需要在被调试代码中添加以下代码:

1
2
3
4
5
6
import debugpy  
  
debugpy.listen(("0.0.0.0", 5678))  
print("Waiting for debugger to attach...")  
debugpy.wait_for_client()  # 阻塞,直到调试器连接  
print("Debugger attached!")  

关键参数说明:

  • debugpy.listen(("0.0.0.0", 5678))
    • 0.0.0.0:允许任意 IP 连接(如果只允许本地连接,用 "localhost")。
    • 5678:默认调试端口(可自定义,但需与 VSCode 配置一致)。
  • debugpy.wait_for_client()
    程序会在此处暂停,直到 VSCode 连接。

同时也要在launch.json中做类似以下的配置:

1
2
3
4
5
6
7
{  
    "name": "Python: Attach",  
    "type": "python",  
    "request": "attach",  
    "host": "localhost",  
    "port": 5678  
}  

由于attach模式的灵活性(不依赖IDE),因此attach模式非常适合远程调试、Docker、分布式。这些情况在采用launch模式进行调试时会面临以下问题:

  • 远程服务器:远程服务器上没有VSCode。
  • Docker 容器:VSCode 无法 launch 容器内的 Python。
  • 分布式训练:进程是由调度系统(不是 IDE)启动的,甚至多个节点。

当然,熟悉VSCode的读者知道,针对前两种情况,VSCode有相应的插件支持,即Remote - SSH 和Docker插件。我们以Remote - SSH 插件为例说明这两个插件的机制(对于Docker插件,把容器类比远程服务器即可):

  1. 本地 VSCode 通过 SSH 登录远程服务器;
  2. 它会在远程服务器上安装一个轻量的 VSCode Server;
  3. 之后你在本地 VSCode 操作的任何“launch”命令,其实是在远程服务器上启动 Python 脚本;
  4. 这样launch模式就可以正常用了,就像在本地一样

那么问题来了:Ray分布式调试有类似的插件使用launch吗?答案是,目前没有。这是因为分布式调试的问题是,Ray是多节点多进程框架,Worker 和 Driver 进程通常由 Ray 集群调度系统启动,IDE 无法统一启动所有节点。所以,Ray调试目前只能基于attach模式。幸运的是,Ray官方提供了一个基于debugpy attach调试的插件Ray Distributed Debugger(RDD),可以轻松的进行调试。

如何用RDD进行本地调试

在讨论如何用RDD进行远程调试前,我们先复习一下文档,看看如何用RDD进行本地调试,更重要的是,看看RDD是如何和Debugpy联系起来的。

调试步骤

步骤1:启动Ray的head节点

可以通过命令行:

1
ray start --head  

也可以在python程序中使用:

1
2
3
4
5
ray.init(  
    runtime_env={  
        "env_vars": {"RAY_DEBUG_POST_MORTEM": "1"},  
    }  
)  

步骤2:打断点

通过breakpoint()在分布式代码中打上断点。官方给出了一个job.py示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import ray  
import sys  
  
# Add the RAY_DEBUG_POST_MORTEM=1 environment variable  
# if you want to activate post-mortem debugging  
ray.init(  
    runtime_env={  
        "env_vars": {"RAY_DEBUG_POST_MORTEM": "1"},  
    }  
)  
  
  
@ray.remote  
def my_task(x):  
    y = x * x  
    breakpoint()  # Add a breakpoint in the Ray task.  
    return y  
  
  
@ray.remote  
def post_mortem(x):  
    x += 1  
    raise Exception("An exception is raised.")  
    return x  
  
  
if len(sys.argv) == 1:  
    ray.get(my_task.remote(10))  
else:  
    ray.get(post_mortem.remote(10))  

步骤3:运行Ray程序

1
python job.py  

运行后你应该会看到类似的三行:

wechat_2025-06-06_005119_009.png

这三个端口分别是:

  • 6379:Ray的GCS (Global Control Store),使用Redis作为后端,用作 Ray 的全局状态存储和调度协调中心。
  • 8265:是 Ray Dashboard 的默认端口。Ray Dashboard 是一个 Web UI,展示集群运行状态、任务执行、资源使用、日志等。更重要的是,虽然调试器会通过6379端口发现集群,但实际调试数据(如堆栈、指标)仍需从 8265 端口获取。
  • 60693:这是RDD为某个Worker 启动的debugpy调试端口,也即debugpy需要去attach的端口,通常是随机的。

步骤4:配置RDD插件

  1. 打开插件面板,点击 “➕ Add Cluster”。
  2. 输入服务端的 Dashboard 地址:127.0.0.1:8265

步骤5:开始调试

  1. 当代码执行到 breakpoint() 时,会在 Ray Debugger 插件中显示暂停任务
  2. 点击 “▶ Start Debug” 启动调试器,进入远程交互调试界面。

多个 breakpoint() 怎么办?

Ray 是多进程框架,一个任务一个进程,因此调试多个 breakpoint() 的步骤如下:

  1. 第一个任务遇到breakpoint()时,attach VSCode调试器
  2. 调试完后,手动断开连接
  3. 任务继续跑,下一次遇到 breakpoint()时,再次通过插件attach

RDD做了什么?

可以看到,RDD相比自己启动debuggy的attach功能,实现了:

  • 你不需要去写launch.json,因为RDD帮你处理好了
  • 在每个 worker 中会自动启动一个 debugpy实例,并动态生成监听端口替代常用的5678端口,相当于:
1
debugpy.listen(("127.0.0.1", random_port))  
  • 你不需要去写侵入式的debugpy代码

如何用RDD进行远程调试

了解了debugpy的原理、RDD的本地调试原理,RDD的远程调试该多做哪些工作就很清楚了:调试端口转发。这里默认是使用了Remote-SSH插件进行的远程连接。

这是因为 Ray 中每个进程的调试端口(如60693)通常绑定在服务器上的 127.0.0.1,本地的 VSCode 根本访问不到这个地址,所以需要通过端口转发来把它“引流”到本地电脑。

具体的,在本地机器上运行:

1
ssh -L 5678:localhost:60593 user@server_ip  

这表示:

  • 本地机器的5678端口被转发到远程服务器上的 127.0.0.1:60593
  • 即使 Ray的debugpy监听的是 127.0.0.1,本地照样可以通过 127.0.0.1:5678 来访问它

如何更优雅的用RDD进行远程调试

按照上文中的方法,每次调试都需要端口转发,当有多个breakpoint()时需要做多次转发,也是很麻烦的。这里推荐一个更优雅的方法:采用WireGuard或者Zerotier等工具将服务器与本地机器组成内网。流程详见RAY远程debug

当组成内网后,服务器与本地机器相当于一个集群中的多机,因此仅需要保证调试端口可以被集群内所有机器访问,而不是只能被本地访问即可。

  1. 在shell中设置:
1
export RAY_DEBUGGER_HOST=0.0.0.0  
  1. 在python代码中修改:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
  
ray.init(  
    runtime_env={  
        'env_vars': {  
            'TOKENIZERS_PARALLELISM': 'true',  
            'NCCL_DEBUG': 'WARN',  
            'RAY_DEBUG': '1'  
        }  
    },  
    dashboard_host='0.0.0.0',  # 启用 Dashboard(默认 8265 端口)  
    include_dashboard=True  
)  

当然,这样做也有缺点,即把调试端口暴露到公网带来了安全风险。

One More thing:直接用debugpy实现远程调试

我们可以看到RDD实际上是对debugpy的一层包装,之前我们提到过debugpy的attach模式本身就适用于远程调试,那么如果不用RDD插件,如何用debugpy实现Ray远程调试呢?

如果你使用的是Remote-SSH插件直接在服务器上进行远程开发(本地没有代码),那么直接用”attach模式”一节中提到的方法即可,因为不用RDD,根本就不会面临插件访问不到调试接口的问题。

在远程情形下,采用RDD的缺点是必须做端口转发;采用debugpy的缺点是必须指定端口、写侵入式代码以及launch.json文件。

comments powered by Disqus
发表了21篇文章 · 总计4万5千字
使用 Hugo 构建
主题 StackJimmy 设计