简易版 pdb watchpoint
本文最后更新于 2026年10月12日
在开发大型项目的时候,经常会遇到某个变量不知道在哪里就被莫名其妙改掉的情况。这时候如果有 gdb 的 watchpoint 那样的功能,在变量被写入时自动断下来打印调用栈,排查起来就会方便很多。
问题背景
以 vLLM(ac0048c0a)为例,FusedMoE 的
forward_native 开头有一段神秘的 padding 代码:
1 | |
但 FusedMoE 初始化的时候明明是
self.hidden_size = 2880,运行时却变成了
3072。所以我们需要一个工具来追踪 self.hidden_size
到底是在哪里被改的。
vLLM 的多进程架构让直接使用 pdb 调试很困难,而且 pdb 似乎也没有像 gdb watchpoint 那样简单直接的功能。
用 property 实现 watchpoint
一个简单的办法是把要追踪的属性改成 property,在 setter
里打印调用栈:
1 | |
这样每次 value
被赋值时,都会打印出旧值、新值和完整的调用栈,一眼就能看到是谁改的。
setter 的原理
property
本质上是一个描述符(descriptor):a.value 会被翻译成
type(a).value.__get__(a),a.value = 10
会被翻译成 type(a).value.__set__(a, 10)。所以只要我们拦截了
__set__,就能拦截所有对这个属性的写入。
@property 装饰器创建一个只读的 property(只定义了
__get__),而 @value.setter 则是在原有
property 的基础上再包一层,把传入的函数注册为
__set__。读操作照常走 getter,写操作则先经过我们的
setter,打印完调用栈再真正写入 self._value。
有两个细节值得注意:
property必须定义在类上才生效。直接obj.value = property(...)挂在实例上是没有用的,因为描述符协议只查类的__dict__。__init__里的初次赋值(self._value = None)如果写的是公开属性名,也会触发 setter。如果不想被初始化干扰,可以在 setter 里加个条件过滤掉。
封装成装饰器
每次手写一遍 property 还是有点啰嗦,可以把这套逻辑封装成一个类装饰器,一行代码就能给任意属性加上 watchpoint:
1 | |
使用方式:
1 | |
对于不方便改源码的第三方库,也可以手动调用装饰器来打补丁:
1 | |
这里把真正的值存到了 _watched_
前缀的属性里,避免和原属性名冲突,也防止 setter 里赋值时无限递归——如果
setter 里直接写 self.value = new_value,就会再次触发 setter
自身,直到栈溢出。