已开启
dtensor debug设计 #295
yangzhenzhang创建于 7月17日
Yyangzhenzhang
7月17日 关联了pull request:feat: add CommDebugMode for DTensor op and collective communication tracing
7月17日 关联了pull request:feat: add CommDebugMode for DTensor op and collective communication tracing
7月20日 关联了pull request:test: 新增MindSpore平台CommDebugMode分布式ST测试用例
7月21日 关联了pull request:test: 将MindSpore平台CommDebugMode ST用例level标记从level0改为level1
7月21日 关联了pull request:test: 将 dtensor ST 用例 level_mark 由 level0 改为 level1
DTensor Debug 能力设计文档
1. 背景
1.1 问题描述
分布式训练中,DTensor 在执行算子时会自动触发 redistribute(重分布)操作,而 redistribute 内部会调用集合通信(all-reduce、all-gather、reduce-scatter、all-to-all 等)。这带来两个核心调试难题:
通信黑盒问题:用户写的是高层 DTensor 算子(如
linear、matmul),框架内部自动产生集合通信,用户无从得知:nn.Module、哪个 DTensor 算子引发的?操作追踪问题:框架调度链路(
OpDispatcher → tensor_redistribution → platform 集合操作)跨越多层,缺少统一的"操作日志",定位问题时需要手动打印大量中间状态。1.2 需求目标
本次工作新增两项调试能力,统一通过
CommDebugMode上下文管理器对外暴露:OpDispatcher.dispatch()入口/出口处挂钩,记录每个 DTensor 算子的名称、输入张量的 shape/placement/mesh 等元信息,形成结构化的调用记录。这两项能力共同构建了"DTensor 算子 → redistribute → 集合通信"这一完整调用链路的可观测性。
2. 当前具体实现方案
2.1 整体架构
记录结果以树形结构保存在
_root_records: List[DebugCall]中,集合通信节点自动嵌套在触发它的 DTensor 算子节点之下。2.2 工作流程
执行期间
_call_stack动态变化,最终结果沉淀到_root_records:2.3 新增/修改文件清单
hyper_parallel/core/dtensor/debug/__init__.pyCommDebugModehyper_parallel/core/dtensor/debug/_comm_debug_mode.pyhyper_parallel/core/dtensor/debug/_call_records.pyhyper_parallel/core/dtensor/debug/_collective_tracer.pyhyper_parallel/core/dtensor/debug/_module_tracker.pyhyper_parallel/core/shard/_op_dispatch.py_debug_mode_observerContextVar 及入口/出口回调hyper_parallel/core/dtensor/__init__.pyCommDebugModehyper_parallel/core/dtensor/tensor_redistribution.py2.4 结构化调用记录(
_call_records.py)定义了一套类层次结构,将所有被追踪的调用统一抽象为
DebugCall:DebugCall(基类) ├── call_depth: int # 嵌套深度 ├── timestamp: float # 创建时间戳 ├── children: List[DebugCall] # 子记录(树形结构) └── render(indent) -> str # 层次化文本渲染 OpCall(DebugCall) # DTensor 算子调用 ├── op_name: str # 算子名,如 "linear"、"mm"、"gelu" ├── input_infos: List[TensorInfo] └── output_infos: List[TensorInfo] CollectiveCall(DebugCall) # 集合通信操作 ├── collective_type: str # 如 "differentiable_all_reduce" ├── group_size: int ├── input_shape / output_shape └── input_dtype: str RedistributeCall(DebugCall) # redistribute 操作(预留) ├── src_placements / dst_placements └── tensor_shape AnnotateCall(DebugCall) # 模块边界标注 ├── module_fqn: str # 模块全限定名 └── event_type: str # "enter" 或 "exit"TensorInfo是张量元信息快照:@dataclass class TensorInfo: shape: Tuple[int, ...] dtype: str is_dtensor: bool = False placements: Optional[Tuple] = None # DTensor 专属 mesh_shape: Optional[Tuple] = None # DTensor 专属2.5 集合操作拦截(
_collective_tracer.py)由于 hyper-parallel 的集合操作直接调用 platform 函数,无法通过算子注册表拦截,采用可恢复 monkey-patch方案:
拦截目标(platform 类的 staticmethod):
关键实现细节:
_patch_lock(threading.Lock)保证多线程安全cls.__dict__[name]保存原始 descriptor(staticmethod包装器),而非绑定函数,保证uninstall()时可精确还原type.__setattr__而非setattr还原,确保 descriptor 协议不被破坏except Exception: pass),保证生产逻辑不受调试代码影响# 安装示例(简化) def _make_wrapper(orig, cb, mname): def wrapper(*args, **kwargs): result = orig(*args, **kwargs) try: cb(mname, args, kwargs, result) except Exception: pass return result return wrapper setattr(cls, name, staticmethod(wrapper)) # 还原 type.__setattr__(cls, name, original_descriptor)2.6 OpDispatcher 钩子(
_op_dispatch.py)在
OpDispatcher.dispatch()中增加了一个ContextVar类型的观察者槽,CommDebugMode 激活时注册自身,退出时清除,对非 debug 场景零开销:# _op_dispatch.py 新增(约 5 行) _debug_mode_observer: ContextVar = ContextVar('_debug_mode_observer', default=None) # dispatch() 入口 observer = _debug_mode_observer.get() if observer is not None: observer._on_op_dispatch_enter(op_name, op_call, args, kwargs) # dispatch() 出口 if observer is not None: observer._on_op_dispatch_exit(op_name, result)CommDebugMode.__enter__时:self._observer_token = _debug_mode_observer.set(self)CommDebugMode.__exit__时:_debug_mode_observer.reset(self._observer_token)利用
ContextVar的 token 机制,天然支持嵌套 CommDebugMode(内层退出后外层自动恢复),无需手动管理嵌套栈。2.7 模块追踪(
_module_tracker.py)ModuleTracker在用户传入module参数时启用,通过register_forward_pre_hook和register_forward_hook对所有子模块打桩:# 为每个子模块注册进入/退出 hook handle_pre = mod.register_forward_pre_hook(_make_pre_hook(fqn)) # 触发 "enter" handle_post = mod.register_forward_hook(_make_post_hook(fqn)) # 触发 "exit"AnnotateCall("enter")压入_call_stack,AnnotateCall("exit")将其弹出,从而使其内部的 DTensor 算子调用和集合通信调用自然成为该模块节点的子节点。2.8 调用树构建机制
_call_stack: List[DebugCall]充当当前嵌套上下文栈:_on_op_dispatch_enter:创建OpCall,若栈非空则挂到栈顶 children,否则挂到 root;压栈_on_op_dispatch_exit:弹栈,填充output_infos_on_collective_call:创建CollectiveCall,挂到栈顶 children(或 root);不压栈(集合通信是叶子节点)_on_module_event("enter"):创建AnnotateCall,压栈_on_module_event("exit"):弹栈由于 platform monkey-patch 的回调在集合通信函数返回后同步触发,此时 OpDispatcher 的 dispatch 尚未退出,
_call_stack中仍有该 DTensor 算子节点,集合通信因此自然成为该算子的子节点,构成正确的因果关系树。3. 对外接口
3.1 导入路径
# 推荐导入 from hyper_parallel.core.dtensor.debug import CommDebugMode from hyper_parallel.core.dtensor import CommDebugMode # 也可通过 dtensor 顶层导入3.2 CommDebugMode 构造参数
CommDebugMode(module=None)modulenn.Module(可选)3.3 上下文管理器协议
with CommDebugMode(module=model) as mode: output = model(input) # 退出后可查询结果__enter__:_debug_mode_observerContextVar(挂载到 OpDispatcher)CollectiveTracer(monkey-patch platform 集合方法)module,安装ModuleTracker并收集初始参数/分片信息__exit__:ModuleTrackerCollectiveTracer(恢复原始 platform 方法)_debug_mode_observerContextVar3.4 查询接口
get_comm_counts()Dict[str, int]{"differentiable_all_reduce": 2}get_total_counts()intget_parameter_info()Dict[str, Dict[str, Any]]module)get_sharding_info()Dict[str, Any]module)3.5 输出接口
generate_comm_debug_tracing_table(noise_level=None) -> str生成格式化的多行追踪表,
noise_level控制输出详细程度(数值越大信息越多):CollectiveCall)OpCall)(默认)AnnotateCall)module)"noise"来自 PyTorch 的命名习惯,意为"信息噪声"——级别越高输出越嘈杂,用户根据调试需要自行权衡。
以下以 ColwiseParallel(fc1) + RowwiseParallel(fc2) MLP 前向为例展示各级别输出差异:
noise_level=0(只看通信):
noise_level=1(看算子 + 通信):
noise_level=2(看模块 + 算子 + 通信,需传入
module=model):别名:
generate_tracing_table(noise_level=None)与此接口等价。log_comm_debug_tracing_table_to_file(file_name="comm_mode_log.txt", noise_level=None) -> None将追踪表写入文件,自动剥离 ANSI 转义码。
generate_json_dump(file_name="comm_mode_log.json", noise_level=None) -> None导出 JSON 格式的完整追踪数据,结构如下:
{ "comm_counts": {"differentiable_all_reduce": 1}, "total_counts": 1, "sharding_info": {"fc2.weight": "Shard(dim=1)"}, "records": [ { "type": "op", "op_name": "linear", "inputs": [{"shape": [4, 32], "dtype": "torch.float32", "is_dtensor": true, "placements": ["Shard(-1)"]}], "outputs": [...], "children": [ {"type": "collective", "collective_type": "differentiable_all_reduce", ...} ] } ] }3.6 完整使用示例
from hyper_parallel.core.dtensor.debug import CommDebugMode # 基本用法:统计集合通信 with CommDebugMode() as mode: output = model(input_dtensor) print(mode.get_comm_counts()) # {'differentiable_all_reduce': 2} print(mode.get_total_counts()) # 2 # 查看详细追踪树(noise_level=1:算子 + 通信) print(mode.generate_comm_debug_tracing_table(noise_level=1)) # 带模块追踪(可定位通信属于哪个 nn.Module) with CommDebugMode(module=model) as mode: output = model(input_dtensor) # 查看模块边界(noise_level=2) print(mode.generate_comm_debug_tracing_table(noise_level=2)) print(mode.get_sharding_info()) # 导出 JSON mode.generate_json_dump("debug_log.json") # 写入文件 mode.log_comm_debug_tracing_table_to_file("debug_log.txt")4. 使用约束
1. 嵌套使用时只有内层有效
当多个
CommDebugMode嵌套使用时,_debug_mode_observerContextVar 始终指向最内层实例,CollectiveTracer的回调也只绑定到最内层,外层在内层存活期间不会收到任何算子或通信事件:with CommDebugMode() as outer: with CommDebugMode() as inner: model(x) # 所有事件只记录到 inner,outer 在此期间为空2. 只追踪经过 OpDispatcher 的算子,只统计经由 platform 的集合通信
调用树中仅包含通过
OpDispatcher.dispatch()分发的 DTensor 算子;_comm_counts仅统计经由 platform 层集合方法(differentiable_all_reduce等)发起的通信。直接通过torch.distributed或其他途径发起的通信不会被捕获。3. 不支持图模式
torch.compile或 MindSpore 图模式下,算子调度路径绕过了OpDispatcher,CommDebugMode无法追踪算子分发,集合通信也可能不经过 platform 层,导致追踪结果为空或不完整。4. 支持 Torch / MindSpore 双平台
CommDebugMode的核心追踪路径(OpDispatcher hook + CollectiveTracer monkey-patch)在两个平台上均可正常工作。MindSpore 平台缺少differentiable_all_to_all_single方法,CollectiveTracer 会安全跳过,不影响其他集合通信的追踪。5.
noise_level=2需要传入module才能生效noise_level=2会在追踪表中显示模块边界标注(Module(fc1) [enter]等),但这依赖ModuleTracker在各子模块上注册的 forward hook。若未向CommDebugMode传入module,ModuleTracker不会被安装,noise_level=2的输出与noise_level=1完全相同,并会打印一条警告日志提示用户。# 正确用法 with CommDebugMode(module=model) as mode: model(x) print(mode.generate_comm_debug_tracing_table(noise_level=2))5. 性能影响分析
5.1 修改范围
本次新增代码均位于
debug/子包中,属于纯新增模块,不影响原有流程。唯一对原有代码的修改是在_op_dispatch.py的dispatch()方法中增加了 3 行。5.2 热路径改动
dispatch()是每个 DTensor 算子必经的热路径,本次在其中新增:# 入口处 observer = _debug_mode_observer.get() if observer is not None: observer._on_op_dispatch_enter(op_name, op_call, args, kwargs) # finally 块中 if observer is not None: observer._on_op_dispatch_exit(op_name, result)5.3 非 debug 模式下的开销(生产训练)
ContextVar.get()if observer is not None每次 dispatch 增加约 2 次
ContextVar.get()+ 2 次 None 判断,合计约 100–400 ns。相比实际 layout 推断、集合通信操作的微秒~毫秒级开销,影响可忽略不计。CollectiveTracer的 monkey-patch 仅在CommDebugMode.__enter__时安装,正常训练时从未调用,对集合通信方法零开销。5.4 debug 模式下的开销
当用户主动使用 CommDebugMode 时,每个算子会额外创建
OpCall对象、执行 list append 和张量 shape/dtype 读取,每次集合通信会额外触发一次 Python 函数调用。这属于调试工具的预期代价,不适用于生产训练。5.5 设计结论
ContextVar是 Python 标准库为"可选激活"场景提供的专用工具,是当前能做到的最低开销方案。非 debug 模式下本次改动对原有训练流程无可感知的性能损耗。6. 测试设计
6.1 测试文件列表
tests/ut/core/dtensor/test_comm_debug_mode.pytests/torch/tensor_parallel/test_comm_debug_mode_mlp.pytorchrun启动 2-rank 进程tests/torch/tensor_parallel/_test_comm_debug_mode_mlp.py6.2 单元测试(
test_comm_debug_mode.py)运行环境:单进程,Torch 平台(
HYPER_PARALLEL_PLATFORM=torch),无需分布式后端。测试分组
TestCallRecordsOpCall、CollectiveCall、DebugCall的_render_self()和render()层次化输出TestCollectiveTracerTestCommDebugModeContextManagerTestObserverHookson_op_dispatch_enter/exit创建 OpCall 并维护树结构、集合通信计数、集合通信嵌套在 Op 下TestDebugOutputgenerate_comm_debug_tracing_table包含层次内容、noise_level=0 过滤 Op、noise_level=1 包含 Op、__repr__TestTensorInfoExtraction_extract_tensor_infos行为TestZeroOverhead_debug_mode_observer默认为None关键测试用例示例
# 验证集合通信嵌套在 Op 节点之下 def test_collective_nested_under_op(self): mode = CommDebugMode() with mode: mode.on_op_dispatch_enter("redistribute", ...) mode._on_collective_call("differentiable_all_gather_concat", ...) mode.on_op_dispatch_exit("redistribute", ...) # 期望:root_records[0].children[0] 是 CollectiveCall assert isinstance(mode._root_records[0].children[0], CollectiveCall) # 验证 noise_level=0 只显示集合通信 def test_tracing_table_noise_level_0(self): table = mode.generate_tracing_table(noise_level=0) assert "differentiable_all_reduce" in table assert "linear" not in table6.3 集成测试(MLP 分布式测试)
运行环境:
torchrun启动,2 个 rank,gloo CPU 后端,ColwiseParallel + RowwiseParallel 张量并行模型。测试模型:
class TwoLayerMLP(nn.Module): def __init__(self, hidden, intermediate): self.fc1 = nn.Linear(hidden, intermediate) # ColwiseParallel self.fc2 = nn.Linear(intermediate, hidden) # RowwiseParallel def forward(self, x): return self.fc2(F.gelu(self.fc1(x)))张量并行策略下的通信分析:
fc1(ColwiseParallel):weightShard(0),inputReplicate,outputShard(-1),无通信fc2(RowwiseParallel):weightShard(1),inputShard(-1),output 从Partial到Replicate,触发 1 次 all-reduce集成测试用例
test_comm_debug_mode_captures_collectivesget_total_counts() >= 1,包含 all-reduce 或 reduce-scattertest_comm_debug_mode_debug_stringgenerate_comm_debug_tracing_table()不为 "(no operations recorded)"test_comm_debug_mode_tracing_tabletest_comm_debug_mode_with_module_trackertest_comm_debug_mode_restores_platformcls.__dict__[name] is orig(identity 检查)test_comm_debug_mode_multiple_forwardsget_total_counts()== 1 次前向的 3 倍6.4 测试覆盖矩阵