DTensor FastPath 技术分析
1. 概述
DTensor 的核心工作流是:拆包参数 → 推断输出分片策略 → 可能做 redistribute → 执行本地算子 → 包装结果。
在 PyTorch 2.9 及之前版本中,这条路径完全在 Python 中执行,入口是 __torch_dispatch__。dispatch 是存在开销的,往往被 __torch_dispatch__ 拦截后在 Python 侧的调库开销更大,甚至可能超过一些小算子的执行时间。
- 关键 PR:https://github.com/pytorch/pytorch/pull/167051
- 核心思想:将操作搬到 C++,在
python_arg_parser.cpp的dispatch_on_subclass层面拦截 DTensor,绕过__torch_dispatch__协议,直接进入 C++ 实现的dispatchDTensorOp。
2. 核心机制
2.1 __torch_dispatch__ 变更至 C++
2.1.1 旧版本 __torch_dispatch__ 拦截逻辑
在旧版中,DTensor 是一个标准的 Tensor subclass,算子分发走的是 PyTorch 的标准 __torch_dispatch__ 协议。先梳理一下 2.9 之前是怎么把 DTensor 全部通过 __torch_dispatch__ 处理的。
当 DTensor 被构造时(通过 _make_dtensor),C++ 会检测它定义了 __torch_dispatch__,于是在 tensor 的 DispatchKeySet 上设置 Python + PythonTLSSnapshot 两个 key:
// python_variable.cpp:2204
if (check_has_torch_dispatch(obj)) {
var.unsafeGetTensorImpl()->set_python_dispatch(true);
}
// c10/core/TensorImpl.h:1991
void set_python_dispatch(bool k) {
if (k) key_set_ = key_set_.add(c10::python_ks);
}
由于携带了 Python key,会被 dispatch 的 PythonFallbackKernel 拦截,通过 PyInterpreter.cpp:305 的 ConcretePyInterpreterVTable::dispatch 执行。
主要作用是提取 Python 对象,通过 handle_torch_function_no_python_arg_parser 分发:
- Step 1(
:558):dispatch_on_mode—— 如果有TorchDispatchMode活跃,先调 mode 的__torch_dispatch__。(这个用法是with xxx,去 call__torch_dispatch__,比如 xray) - Step 2(
:577):dispatch_on_subclass—— 调 subclass(DTensor)的__torch_dispatch__。(这个用法是 Tensor 子类,比如 DTensor)
static py::object dispatch_on_subclass(...) {
for (auto& arg : overloaded_args) { // 遍历所有 DTensor 参数
// 第 322 行:从 arg 上取 "__torch_dispatch__" 属性
py::object torch_function =
PyObject_FastGetAttrString(arg, torch_function_name_str);
// ↑ "__torch_dispatch__"
// ... 跳过 disabled 的情况 ...
// 第 349-369 行:先检查有没有注册的 per-op dispatch rule
// 对于普通 DTensor 没有,所以跳过
ret = py::reinterpret_steal<py::object>(PyObject_CallFunctionObjArgs(
torch_function.ptr(), // DTensor.__torch_dispatch__ (bound method)
torch_api_function, // torch.ops.aten.add.Tensor
py_types.ptr(), // (DTensor,)
args, // (dtensor_a, dtensor_b)
kwargs, // {}
NULL));
从这里,C++ 通过 CPython 的 C API 去调用 Python 解释器执行这个 Python 函数,这也是为什么旧版本慢——每次算子调用都要从 C++ “绕回” Python 解释器去执行 __torch_dispatch__,而新版的 fast path 就是为了尽量不绕回 Python。
2.1.2 新版本改动
// 检查完 per-op dispatch rule 后,加了一个 if 判断:
+ if (!is_torch_function && is_dtensor(arg)) {
+ ret = dispatchDTensorOp(
+ *opt_op, torch_api_function, args, kwargs, opt_stack);
+ } else {
ret = PyObject_CallFunctionObjArgs(
torch_function.ptr(),
torch_api_function,
py_types.ptr(),
args, kwargs, NULL);
+ }
dispatchDTensorOp 是一个与旧版本 __torch_dispatch__ 等价的计算流程,只不过用 C++ 改写了。概览:
dispatchDTensorOp(op, args, kwargs, stack)
│
├─ A: custom_op_handlers 命中? → 直接调 handler,return
│
├─ B: C++ cache 命中?
│ ├─ YES → cached_sharding 有值,跳到 D
│ └─ NO → 进入 C
│
├─ C: 调 Python propagation,缓存结果
│ └─ decompose 了? → 直接 return 结果
│
├─ D: 执行本地算子
│ ├─ 不需要 redistribute 且非随机算子?
│ │ → C++ callBoxed() ← 全 C++,零 Python
│ └─ 否则 → 调 Python slow path
│
└─ E: 包装结果
├─ 普通/view 算子? → wrap + C++ alias 处理
└─ inplace/out 算子? → 调 Python tail 处理
阶段 A:custom_op_handlers(优先级最高)
这里的逻辑不变,依然是全局优先级最高的,调用的也是 Python 方法。
// C++ 侧
const auto custom_op_handlers =
op_dispatcher.attr(dtensor_interned_strings._custom_op_handlers);
PyObject* custom_op_handler =
PyDict_GetItemWithError(custom_op_handlers.ptr(), py_op.ptr());
if (custom_op_handler) {
auto result = checked_vectorcall(
custom_op_handler, py_op.ptr(), args.ptr(), kwargs.ptr());
stack->clear();
return result; // 命中就直接返回,后面全跳过
}
# 等价的 Python 逻辑
if op_call in self._custom_op_handlers:
return self._custom_op_handlers[op_call](op_call, args, kwargs)
阶段 B:C++ Cache 查找
这一部分会尝试读缓存,如果存在 native_sharding_propagator_cache,就无需重新执行阶段 C。
create_native_op_schema 遍历 stack 上的参数,为每个 DTensor 提取 DTensorSpec(placement + shape),组成 NativeOpSchema 作为 hash key。然后在线程局部的 C++ HashMap 中查找。
- 命中:
cached_sharding有值 → 跳过整个阶段 C(不调任何 Python) - 未命中:
cached_sharding为空 → 进入阶段 C - 构建 key 失败(如有 SymInt):
opt_native_op_schema为nullopt→ 也进入阶段 C
// 尝试在 C++ 中构建一个精简的缓存 key
auto opt_native_op_schema = create_native_op_schema(op, py_op, stack);
if (opt_native_op_schema.has_value()) {
native_sharding_propagator_cache =
&get_thread_local_native_sharding_propagator_cache();
cached_sharding =
native_sharding_propagator_cache->find(opt_native_op_schema->first);
}
阶段 C:回退 Python 做 sharding propagation
if (!cached_sharding) {
// 1. 调 Python 构建 OpInfo(unwrap DTensor 参数)
py_op_info = checked_vectorcall(
op_dispatcher.attr("unwrap_to_op_info").ptr(),
py_op.ptr(), args.ptr(), kwargs.ptr());
// 2. 调 Python 做 sharding propagation
py::object sharding = checked_vectorcall(
op_dispatcher.attr("_propagate_op_sharding_dispatch_slow_path").ptr(),
py_op.ptr(), args.ptr(), kwargs.ptr(), py_op_info.ptr(),
/*try_cache*/ !opt_native_op_schema.has_value() ? Py_True : Py_False);
# 等价的 Python 逻辑
op_info = self.unwrap_to_op_info(op_call, args, kwargs)
self.sharding_propagator.propagate(op_info)
如果算子没有注册策略但能 decompose,propagation 内部会 decompose 并直接返回结果。C++ 这里通过类型检查区分"返回的是 OutputSharding 还是直接结果":
// 3. propagation 有时直接返回 decompose 的结果(不是 OutputSharding)
if (!py::isinstance(sharding, get_output_sharding_class())) {
stack->clear();
return sharding; // 直接返回 tensor 结果
}
# 等价的 Python 逻辑
except NotImplementedError:
if has_kernel_for(CompositeImplicitAutograd):
out = op_call.decompose(*args, **kwargs)
return out
这是新版的核心收益:首次 miss 时计算结果,缓存起来,后续相同 op + placement 组合直接命中阶段 B:
// 4. 缓存结果,下次就走阶段 B 的快路径
cached_sharding = sharding;
if (opt_native_op_schema.has_value()) {
native_sharding_propagator_cache->insert(
std::move(opt_native_op_schema->first), std::move(sharding));
}
阶段 D:获取 compute_mesh + 执行本地算子
// 获取 compute_mesh
py::object compute_mesh;
if (opt_native_op_schema.has_value()) {
compute_mesh = std::move(opt_native_op_schema->second); // C++ 已经拿到了
} else {
get_py_op_info_if_needed(); // 回退 Python 取
compute_mesh = py_op_info.attr("compute_mesh");
}
// 执行本地算子
const bool local_results_success =
get_local_results(op, cached_sharding, compute_mesh, participating, stack);
if (local_results_success) {
// C++ 快路径成功:stack 上已经是本地计算结果
py_local_results = torch::jit::createPyObjectForStack(std::move(*stack));
} else {
// 回退 Python(需要 redistribute 或是随机算子)
get_py_op_info_if_needed();
py_local_results = checked_vectorcall(
get_dtensor_get_local_results_slow_path().ptr(), ...);
}
阶段 E:包装结果
C++ 侧有一系列限定条件,如果不满足,还是会回退 Python 实现:
if (!is_inplace_op && !is_out_variant_op &&
!(output_spec.is_none() &&
(op.operator_name().name == "aten::equal" &&
is_default_overload(op.operator_name().overload_name)))) {
// C++ fast path: wrap + alias 处理
} else {
// 回退 Python tail 处理
}
2.2 Cache 详解
2.2.1 Cache 的数据结构
NativeShardingPropagatorCache
└── c10::FastMap<NativeOpSchema, py::object>
↑ key ↑ value
| 组成 | 说明 |
|---|---|
| Key | NativeOpSchema — 描述"这个算子,配上这些 placement 和参数,是什么样的调用" |
| Value | py::object — 一个 Python OutputSharding 对象,包含 sharding propagation 的完整结果(output_spec、redistribute_schema 等) |
| 存储位置 | thread_local,每个线程一份独立的 cache(无锁,无跨线程竞争) |
2.2.2 Cache Key — NativeOpSchema 由什么组成
NativeOpSchema 的相等和哈希由三部分决定(python_variable.cpp:1117-1123):
bool operator==(const NativeOpSchema& rhs) const {
return op_ == rhs.op_ // (1) 算子
&& args_schema_len_ == rhs.args_schema_len_ // (2) 参数数量
&& comparison_key_ == rhs.comparison_key_; // (3) 参数特征
}
具体来说:
op_—c10::OperatorHandle,唯一标识一个算子,比如aten::add.Tensor、aten::mm.default。args_schema_len_— 参数列表长度。comparison_key_—SmallVector<IValueOrDTensorSpec, 8>,这是核心。遍历所有参数,对每个参数按类型生成 key 元素:
| 参数类型 | 放入 comparison_key 的内容 |
|---|---|
| DTensor | dtensor._spec(即 DTensorSpec Python 对象,包含 placements + mesh + tensor_meta) |
| 普通 Tensor | 为其构造一个 replicate 的 DTensorSpec |
| int/float/bool 等标量 | 仅当 idx >= static_argnum 时放入(即非静态参数才参与比较) |
List(如 torch.cat 的输入列表) | 先放列表长度,再逐元素递归处理 |
关键点:
DTensorSpec的比较用的是 Python 的__eq__,所以只要 placements 和 mesh 相同,就视为等价。tensor 的具体 shape/stride/数据不参与 key 比较(前提是RuntimeSchemaInfo将对应参数标为 static)。
2.2.3 命中条件
Cache HIT 条件(所有条件同时满足):
- 同一个算子(
OperatorHandle相同,比如都是aten::add.Tensor) - 所有 DTensor 参数的
DTensorSpec相同(placements 和 mesh 一致) - 非 static 标量参数值相同(比如
alpha=1vsalpha=2是不同 key) - 参数个数相同
- 没有 SymInt(有 SymInt 直接跳过 cache)
Cache MISS 场景:
- 第一次调用某个算子 + placement 组合 → 必然 miss
- 同一算子但 placement 不同:
torch.add(Shard(0), Shard(0))vstorch.add(Replicate(), Replicate())→ miss - 同一算子但非 static 参数变化:某些算子的 dim 参数等
- DTensor 含 SymInt → 直接 bail out 到 Python path,不查 cache
RuntimeSchemaInfo标记needs_pytree=True且 fast path 未启用 → bail out
2.2.4 Cache Value — OutputSharding 包含什么
Cache 存储的 OutputSharding Python 对象包含:
| 字段 | 说明 |
|---|---|
output_spec | 输出的 DTensorSpec(placements + mesh) |
redistribute_schema | 如果输入需要重新分布,描述怎么 redistribute(None 说明不需要) |
needs_redistribute | 是否需要在执行前 redistribute 输入 |
当 cache hit 时,后续流程直接看 output_sharding 决定:
- 无需 redistribute → C++ 直接执行 local op(
get_local_results :1369),全程不碰 Python - 需要 redistribute → 回退到 Python slow path 做 redistribute + 执行