DeepEP 测试基础设施与通用工具深度分析
1. 概述
DeepEP 的测试基础设施是一个面向多 GPU 分布式 Expert Parallelism (EP) 场景的全栈验证系统,涵盖分布式进程组初始化、参考实现(Reference Implementation)、数值精度验证、性能基准测试(Benchmark)、硬件带宽探测等核心模块。测试设计的核心哲学是 "bitwise identical" —— 对于确定性算法,要求 GPU kernel 输出与 PyTorch 参考实现逐比特一致,而非传统的误差容忍比较。
文件结构总览
DeepEP/
├── tests/
│ ├── elastic/
│ │ ├── test_ep.py # 核心: Dispatch/Combine 正确性 + 性能测试
│ │ ├── test_agrs.py # AllGather/ReduceScatter 测试
│ │ ├── test_barrier.py # Barrier 同步原语测试
│ │ ├── test_engram.py # Engram 内存管理测试
│ │ └── test_pp.py # Pipeline Parallelism 测试
│ ├── legacy/ # 旧版 (V1) 测试
│ └── utils/ # 工具测试
└── deep_ep/utils/
├── __init__.py # 导出 EventHandle
├── testing.py # bench / bench_kineto / flush_l2_cache
├── gate.py # 非均衡 MoE 流量生成 (Unbalanced Score)
├── comm.py # NCCL Communicator 封装
├── envs.py # 分布式初始化 / 硬件探测
├── event.py # EventOverlap CUDA 事件封装
├── refs.py # PyTorch 参考实现 (dispatch / combine)
├── math.py # FP8 编解码 / 对齐 / 差异计算
├── semantic.py # weak_lru / value_or
└── find_pkgs.py # NVIDIA 包路径探测
2. 测试初始化:分布式进程组构建
2.1 init_dist —— 分布式环境初始化
envs.py:73-113 定义了测试入口的分布式初始化流程:
# envs.py:73-113
def init_dist(local_rank: int, num_local_ranks: int, seed: int = 0) -> Tuple[int, int, dist.ProcessGroup]:
ip = os.getenv('MASTER_ADDR', '127.0.0.1')
port = int(os.getenv('MASTER_PORT', '8361'))
num_nodes = int(os.getenv('WORLD_SIZE', 1))
node_rank = int(os.getenv('RANK', 0))
sig = inspect.signature(dist.init_process_group)
params = {
'backend': 'nccl',
'init_method': f'tcp://{ip}:{port}',
'world_size': num_nodes * num_local_ranks,
'rank': node_rank * num_local_ranks + local_rank,
}
if 'device_id' in sig.parameters:
params['device_id'] = torch.device(f'cuda:{local_rank}')
dist.init_process_group(**params)
torch.set_default_dtype(torch.bfloat16)
torch.set_default_device('cuda')
torch.cuda.set_device(local_rank)
init_seed(seed)
return dist.get_rank(), dist.get_world_size(), dist.new_group(list(range(num_local_ranks * num_nodes)))
关键设计要点:
| 要素 | 实现 | 说明 |
|---|---|---|
| 后端 | nccl |
强制使用 NCCL 后端,匹配生产环境 |
| 默认 dtype | torch.bfloat16 |
全局设定,所有未指定 dtype 的张量默认为 BF16 |
| 默认 device | cuda |
避免 CPU/GPU 混用导致的隐性同步 |
device_id 兼容 |
inspect.signature 探测 |
适配不同 PyTorch 版本(新版的 device_id 参数) |
| 全局 group | dist.new_group(list(range(N))) |
创建包含所有 rank 的全局通信组 |
2.2 测试启动流程
test_ep.py:564-609 使用 torch.multiprocessing.spawn 启动多进程测试:
# test_ep.py:609
torch.multiprocessing.spawn(test_loop, args=(num_processes, args), nprocs=num_processes)
2.3 初始化流程图
flowchart TD
A["__main__: argparse 解析参数"] --> B["torch.multiprocessing.spawn<br/>启动 N 个 test_loop 进程"]
B --> C["init_dist: 设置 MASTER_ADDR/PORT"]
C --> D["dist.init_process_group<br/>(NCCL backend)"]
D --> E["torch.set_default_dtype(bfloat16)<br/>torch.set_default_device(cuda)"]
E --> F["init_seed: global_seed + rank"]
F --> G["dist.new_group 创建全局 group"]
G --> H["construct_elastic_buffer<br/>构建 ElasticBuffer"]
H --> I["test_dispatch_combine<br/>执行正确性 + 性能测试"]
I --> J{"do_pressure_test?"}
J -->|Yes| K["循环 seed in range(1e9)<br/>重建 buffer 并重复测试"]
J -->|No| L["buffer.destroy()"]
K --> L
L --> M["dist.destroy_process_group()"]
3. testing.py —— 性能基准测试工具集
testing.py 提供了三个核心工具:flush_l2_cache、bench、bench_kineto,以及一个 IO 抑制上下文管理器。
3.1 flush_l2_cache —— L2 Cache 刷新
# testing.py:12-21
def flush_l2_cache(enabled: bool = True):
l2_flush_cache_size = 256e6
if enabled:
torch.empty(int(l2_flush_cache_size // 4), dtype=torch.int, device='cuda').zero_()
原理:分配 256MB / 4 = 64M 个 int(256MB)零初始化张量,用大量无效数据冲刷 L2 Cache,确保每次 benchmark 迭代从相同的缓存状态开始。这是 GPU benchmark 的标准做法 —— 避免前一次迭代的残留缓存影响当前迭代的命中率。
3.2 bench —— 基于 CUDA Event 的粗粒度 Benchmark
# testing.py:24-60
def bench(fn, num_warmups: int = 50, num_tests: int = 50,
post_fn: Optional[Callable] = None, flush_l2: bool = True):
torch.cuda.synchronize()
for _ in range(num_warmups):
fn()
start_events = [torch.cuda.Event(enable_timing=True) for _ in range(num_tests)]
end_events = [torch.cuda.Event(enable_timing=True) for _ in range(num_tests)]
for i in range(num_tests):
flush_l2_cache(flush_l2)
start_events[i].record()
fn()
end_events[i].record()
if post_fn is not None:
post_fn()
torch.cuda.synchronize()
times = np.array([s.elapsed_time(e) / 1e3 for s, e in zip(start_events, end_events)])[1:]
return np.average(times), np.min(times), np.max(times)
设计要点:
- Warmup 50 次:消除 JIT 编译、CUDA context 初始化等一次性开销
- 跳过首次测量:times[1:] 排除第一次可能存在的延迟峰值
- 返回 avg/min/max:提供统计分布信息
3.3 bench_kineto —— 基于 PyTorch Profiler 的细粒度 Kernel 级 Benchmark
这是测试框架中最精巧的工具,用于测量单个 CUDA kernel 的执行时间:
# testing.py:111-219
def bench_kineto(fn, kernel_names: Union[str, tuple], num_tests: int = 30,
suppress_kineto_output: bool = False, trace_path: Optional[str] = None,
flush_l2: bool = True, barrier_comm_profiling: bool = False,
num_kernels_per_period: int = 1, barrier: Optional[Callable] = None):
核心流程:
flowchart TD
A["检查 EP_USENVIDIA_TOOLS 环境变量"] -->|已设置| Z["返回 1 (跳过)"]
A -->|未设置| B["fn() 预热"]
B --> C["torch.profiler.profile 配置<br/>schedule: wait=0, warmup=1, active=1"]
C --> D["循环 num_tests 次"]
D --> E["flush_l2_cache"]
E --> F{"barrier_comm_profiling?"}
F -->|Yes| G["torch.cuda._sleep(2e7)<br/>dist.all_reduce 同步"]
F -->|No| H["fn()"]
G --> H
H --> I["profiler.step()"]
I --> J["解析 key_averages().table()"]
J --> K["匹配 kernel_names<br/>提取 time × count"]
K --> L{"num_kernels_per_period > 1?"}
L -->|Yes| M["Chrome trace JSON 解析<br/>按时间戳排序分段聚合"]
L -->|No| N["返回平均 duration"]
M --> N
关键设计:
- 与 Nsight 工具互斥(L143-144):检测到
EP_USE_NVIDIA_TOOLS时直接跳过,避免 profiler 冲突 - Barrier 消除 CPU 发射不均(L164-171):
torch.cuda._sleep(2e7)(~10ms 大 kernel) +dist.all_reduce同步所有 rank 的 CPU 发射 - 多 kernel 周期展开(L205-216):当单次
fn()调用会发射多个同类 kernel 时(如 dispatch 的dispatch_impl+dispatch_copy_epilogue_impl),从 Chrome trace 按时间戳排序后分段计算每个 kernel pattern 的平均耗时
3.4 suppress_stdout_stderr —— IO 抑制
# testing.py:72-108
class suppress_stdout_stderr:
def __enter__(self):
self.outnull_file = open(os.devnull, 'w')
self.errnull_file = open(os.devnull, 'w')
self.old_stdout_fileno = os.dup(sys.stdout.fileno())
self.old_stderr_fileno = os.dup(sys.stderr.fileno())
os.dup2(self.outnull_file.fileno(), self.old_stdout_fileno_undup)
os.dup2(self.errnull_file.fileno(), self.old_stderr_fileno_undup)
...
通过 os.dup2 将 stdout/stderr 重定向到 /dev/null,用于抑制 PyTorch profiler 的冗余输出。使用底层文件描述符操作而非简单的 sys.stdout 赋值,确保 C 扩展层的输出也被抑制。
4. gate.py —— 非均衡 MoE 流量生成
gate.py 是测试数据生成的核心,专门用于模拟非均衡 Expert 选择(Unbalanced Routing),这是 MoE 系统中最具挑战性的场景。
4.1 问题定义
在真实 MoE 负载中,某些 Expert 会被过度选择(Hot Expert 问题),导致通信不均。测试框架需要可控地生成这种非均衡分布:
- ratio = 1.0:完全均衡
- ratio > 1.0:某个特殊 rank 的流量是其他 rank 的
ratio倍
4.2 两种生成策略
策略 A:get_precise_unbalanced_scores —— 精确非均衡
# gate.py:116-137
def get_precise_unbalanced_scores(num_tokens, num_experts, num_ranks, num_topk, ratio):
rank_count = generate_rank_count(num_tokens, num_experts, num_ranks, num_topk, ratio)
threshold = 0.9
scores = torch.empty((num_tokens, num_experts), dtype=torch.float32, device='cuda')
scores.uniform_(to=threshold) # 低分填充
topk_idx = generate_topk_idx(rank_count, ...)
topk_scores = torch.empty((num_tokens, num_topk), ...)
topk_scores.uniform_(threshold + 1e-6, 1.0) # 高分填充
scores[row_idx, topk_idx] = topk_scores
return scores
算法流程:
1. generate_rank_count:先生成每个 token 选择各 rank 的 Expert 数量分布(一个 (num_tokens, num_ranks) 的 count 矩阵)
2. generate_topk_idx:将 count 映射为具体的 Expert 索引,通过随机排列避免模式化
3. 高低分分离:被选中的 Expert 给 0.9-1.0 高分,其余给 0-0.9 低分,确保 topk 选择的确定性
策略 B:get_random_unbalanced_scores —— 随机非均衡
# gate.py:167-173
def get_random_unbalanced_scores(num_tokens, num_experts, num_ranks, num_topk, ratio):
factor = 1.0
if ratio != 1.0:
factor = map_unbalanced_ratio_to_factor(num_tokens, num_experts, num_ranks, num_topk, ratio)
return get_scores_by_factor(num_tokens, num_experts, num_ranks, factor)
使用二分搜索(map_unbalanced_ratio_to_factor,L148-164)找到使 rank 0 流量达到 ratio 倍的 factor 值:
# gate.py:148-164
def map_unbalanced_ratio_to_factor(...):
factor_l, factor_r = 1.0, 100.0
for _i in range(num_iterations): # 20 次迭代
factor_mid = (factor_l + factor_r) / 2
scores = get_scores_by_factor(...)
_, topk_idx = torch.topk(scores, num_topk, ...)
rank_idx = topk_idx // num_experts_per_rank
one_hot = torch.nn.functional.one_hot(rank_idx, num_ranks)
counts = one_hot.any(dim=1).to(torch.float).sum(dim=0)
if counts[0].item() > counts[1:].mean().item() * ratio:
factor_r = factor_mid
else:
factor_l = factor_mid
return factor_l
4.3 generate_rank_count —— 核心分布生成算法
# gate.py:32-113
def generate_rank_count(num_tokens, num_experts, num_ranks, num_topk, ratio):
# 1. 随机 topk 选择,计算每个 token 覆盖的 rank 数 a[i]
random_scores = torch.rand(num_tokens, num_experts, device='cuda')
topk_weights_, topk_indices = torch.topk(random_scores, num_topk, ...)
topk_indices //= num_experts_per_rank
sorted_topk_indices = torch.sort(topk_indices, dim=1)[0]
topk_indices_diff_mask = sorted_topk_indices[:, 1:] != sorted_topk_indices[:, :-1]
a = topk_indices_diff_mask.sum(dim=1) + 1
# 2. 计算特殊 rank 必须/可选出现的 token 数
upper_bound_per_token = int(num_normal_ranks / ratio) + 1
a = torch.clamp(a, None, upper_bound_per_token)
must_mask = (a == num_ranks)
...
# 3. 生成 rank 排列并插入特殊 rank
rank_perm = (torch.randperm(num_normal_ranks, device='cuda') + 1).repeat(...)
rank_perm_with_special_rank = torch.zeros(num_tokens * num_topk, ...)
# ... 复杂的索引计算 ...
# 4. scatter_add 生成最终 count
rank_count = torch.zeros((num_tokens, num_ranks), dtype=torch.int32, device='cuda')
rank_count.scatter_add_(dim=1, index=result, src=torch.ones_like(result, dtype=torch.int32))
return rank_count
算法本质:通过控制"特殊 rank 必须出现在 topk 中的 token 数量"来精确调控流量倾斜比,同时保持随机性以避免测试偏差。
4.4 数据生成流程图
flowchart TD
A["get_unbalanced_scores<br/>(ratio, precise)"] -->|precise=True| B["get_precise_unbalanced_scores"]
A -->|precise=False| C["get_random_unbalanced_scores"]
B --> D["generate_rank_count<br/>生成 (num_tokens, num_ranks) count 矩阵"]
D --> E["generate_topk_idx<br/>count → 具体 Expert 索引"]
E --> F["高低分分离填充 scores"]
C --> G["map_unbalanced_ratio_to_factor<br/>二分搜索 factor"]
G --> H["get_scores_by_factor<br/>rank 0 用 factor 加权"]
F --> I["返回 scores (num_tokens, num_experts)"]
H --> I
5. comm.py —— NCCL Communicator 封装
5.1 NCCLCommHandle —— 生命周期管理
# comm.py:11-37
class NCCLCommHandle:
def __init__(self, nccl_comm: int, managed: bool):
self.nccl_comm = nccl_comm
self.managed = managed
self.destroy = _C.destroy_nccl_comm
def __del__(self):
if self.managed:
self.destroy(self.nccl_comm)
DeepEP 的 C++ 层需要直接操作 NCCL communicator 的裸指针(int 类型)。NCCLCommHandle 封装了这个指针的生命周期:
- managed=True:DeepEP 自己创建的 comm,__del__ 时调用 _C.destroy_nccl_comm 销毁
- managed=False:复用 PyTorch 的 comm,不主动销毁
5.2 get_nccl_comm_handle —— 缓存与复用
# comm.py:42-75
_storage = dict()
def get_nccl_comm_handle(group: dist.ProcessGroup, force_new_comm: bool = False) -> NCCLCommHandle:
global _storage
if not force_new_comm and group in _storage:
return _storage[group]
backend = group._get_backend(torch.device('cuda'))
if not force_new_comm and hasattr(backend, '_comm_ptr') and int(os.getenv('EP_REUSE_NCCL_COMM', '1')):
_storage[group] = NCCLCommHandle(backend._comm_ptr(), False)
return _storage[group]
# 旧版 PyTorch: 手动 all_gather unique_id 后创建新 comm
nccl_unique_ids = [None, ] * group.size()
dist.all_gather_object(nccl_unique_ids, _C.get_local_nccl_unique_id(), group)
root_unique_id = nccl_unique_ids[0]
key = time.time_ns() if force_new_comm else group
_storage[key] = NCCLCommHandle(
_C.create_nccl_comm(root_unique_id, group.size(), group.rank()), True)
return _storage[key]
三种获取路径:
| 场景 | 行为 | 说明 |
|---|---|---|
| 缓存命中 | 直接返回 | 避免重复创建 |
PyTorch 有 _comm_ptr |
复用,managed=False |
现代 PyTorch 默认路径 |
| 旧版 PyTorch | all_gather_object + 新建 |
兼容性回退 |
EP_REUSE_NCCL_COMM=1(默认)控制是否允许复用 PyTorch 的 communicator。force_new_comm=True 用于需要独立 comm 的场景(避免与 PyTorch 的通信冲突)。
6. envs.py —— 环境探测与硬件检测
6.1 种子管理
# envs.py:24-35
def init_seed(global_seed: int) -> None:
global _local_seed, _global_seed
_local_seed = global_seed + dist.get_rank()
_global_seed = global_seed
torch.manual_seed(_local_seed)
random.seed(_local_seed)
设计:local_seed = global_seed + rank,确保每个 rank 生成不同的随机数据(这对 EP 正确性至关重要 —— 不同 rank 必须有不同的 token 和路由),同时保持可复现性。
6.2 dist_print —— 分布式安全打印
# envs.py:58-70
def dist_print(s: str = '', once_in_node: bool = False) -> None:
global _local_rank
assert _local_rank is not None
if not once_in_node or _local_rank == 0:
print(s, flush=True)
dist.barrier()
关键:末尾有 dist.barrier(),确保所有 rank 同步到达后才继续,避免某 rank 的打印阻塞其他 rank 的等待。once_in_node=True 时仅 local_rank 0 打印,减少多节点场景下的输出冗余。
6.3 物理 / 逻辑域大小获取
# envs.py:116-142
def get_physical_domain_size(group) -> Tuple[int, int]:
return _C.get_physical_domain_size(get_nccl_comm_handle(group).get())
def get_logical_domain_size(group, allow_hybrid_mode=True) -> Tuple[int, int]:
return _C.get_logical_domain_size(get_nccl_comm_handle(group).get(), allow_hybrid_mode)
- 物理域:
(num_rdma_ranks, num_nvlink_ranks)—— 硬件拓扑决定 - 逻辑域:
(num_scaleout_ranks, num_scaleup_ranks)—— 由 EP 配置决定,allow_hybrid_mode控制是否允许混合模式
6.4 硬件带宽探测
NVLink 带宽
# envs.py:192-219
@functools.lru_cache()
def get_nvlink_gbs(factor: float = 0.9) -> float:
result = subprocess.run(['nvidia-smi', 'nvlink', '-s'], ...)
pattern = r'GPU \d+:.*?(?=^GPU \d+:|^$)'
match = re.search(pattern, output, re.MULTILINE | re.DOTALL)
gpu_block = match.group(0)
link_pattern = r'Link \d+:\s*([\d\.]+) GB/s'
link_matches = re.findall(link_pattern, gpu_block)
return sum(float(bw) for bw in link_matches) * factor
解析 nvidia-smi nvlink -s 输出,汇总所有 Link 的带宽后乘以 factor=0.9(效率系数)。
RDMA 带宽
# envs.py:245-268
@functools.lru_cache()
def get_rdma_gbs(nic_name: str = _DEFAULT_NIC_NAME) -> float:
result = subprocess.run(['ibstat'], ...)
pattern = rf"CA '{nic_name}'.*?Port \d+:\s*.*?Rate:\s*(\d+)"
match = re.search(pattern, output, re.DOTALL)
rate = int(match.group(1))
return rate / 8 # Gb/s → GB/s
解析 ibstat 输出,提取指定 NIC(默认 mlx5_0)的端口速率。rate / 8 将 Gb/s 转为 GB/s。
快速 RDMA 原子操作检测
# envs.py:222-242
@functools.lru_cache()
def check_fast_rdma_atomic_support(nic_name: str = _DEFAULT_NIC_NAME) -> bool:
result = subprocess.run(['ibstat'], ...)
pattern = rf"CA '{nic_name}'.*?CA type:\s*(\S+)"
match = re.search(pattern, output, re.DOTALL)
return match.group(1) == 'MT4131'
检测 NIC 是否为 MT4131 型号(支持快速 RDMA 原子操作),用于决定是否启用某些低延迟优化路径。
6.5 其他环境检测
| 函数 | 功能 |
|---|---|
check_nvlink_connections |
PCIe GPU 场景下用 pynvml 检查 NVLink P2P 连通性 |
check_torch_deterministic |
确保 deterministic_algorithms 和 fill_uninitialized_memory 不同时为 True(会导致 torch.empty 初始化 kernel 与通信 stream 冲突) |
7. event.py —— EventOverlap CUDA 事件封装
7.1 类设计
# event.py:8-96
class EventOverlap:
def __init__(self, event: Optional[EventHandle] = None,
extra_tensors: Optional[Tuple[torch.Tensor]] = None):
self.event = event
self.extra_tensors = extra_tensors
self._release_handle_by_call = False
self.hook_after_wait: Optional[Callable] = None
7.2 核心方法
current_stream_wait —— 流同步 + Hook 触发
# event.py:39-54
def current_stream_wait(self, release_handle: bool = False) -> None:
assert self.event is not None
self.event.current_stream_wait()
if self.hook_after_wait is not None:
self.hook_after_wait()
self.hook_after_wait = None
if release_handle:
self.event = None
Hook 机制:hook_after_wait 允许在等待完成后执行回调,用于 deterministic dispatch 场景 —— 通信完成后需要在当前 stream 上执行排序操作。
上下文管理器模式 —— 计算与通信重叠
# event.py:74-96
def __enter__(self) -> Any:
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
if self.event is not None:
self.current_stream_wait(release_handle=self._release_handle_by_call)
self._release_handle_by_call = False
使用模式:
event_overlap = event_after_all_to_all_kernels()
with event_overlap:
do_something_on_current_stream() # 与通信并行执行
# 退出 with 时自动 current_stream_wait
7.3 extra_tensors —— CUDA Graph 兼容的 Stream Recording
注释说明(L27-29):extra_tensors 用于模拟 PyTorch tensor 的 record_stream 行为。标准 record_stream 与 CUDA Graph 不兼容,通过持有 tensor 的额外引用来延迟释放,确保事件等待完成前 tensor 不被回收。
7.4 EventOverlap 使用模式图
sequenceDiagram
participant Compute as Compute Stream
participant Comm as Communication Stream
participant Event as EventOverlap
Compute->>Comm: dispatch / combine 启动
Comm->>Event: event.capture()
Compute->>Compute: do_something_on_current_stream()
Compute->>Event: __exit__ → current_stream_wait()
Event->>Comm: wait for event
Comm->>Compute: 通信完成,继续执行
8. refs.py —— PyTorch 参考实现
参考实现(Reference Implementation)是测试正确性的金标准。DeepEP 的 GPU kernel 输出必须与参考实现 bitwise identical。
8.1 ref_dispatch —— Dispatch 参考实现
# refs.py:10-123
def dispatch(x, topk_idx, topk_weights, num_max_tokens_per_rank, num_experts):
# 1. 按目标 rank 分组发送数据
for dst_rank_idx in range(num_ranks):
expert_start_idx = dst_rank_idx * num_experts_per_rank
expert_end_idx = expert_start_idx + num_experts_per_rank
mask_to_send = ((expert_start_idx <= topk_idx) & (topk_idx < expert_end_idx)).any(dim=1)
indices_to_send = mask_to_send.nonzero(as_tuple=True)[0]
# ... 选择 x, sf, topk_idx, topk_weights ...
# 2. all_to_all_single 交换大小和数据
dist.all_to_all_single(num_recv_tokens_per_rank, num_send_tokens_per_rank)
dist.all_to_all_single(recv_x, send_x, ...)
# ... 其他张量同理 ...
# 3. 本地 Expert 索引转换 + mask
recv_topk_idx = recv_topk_idx - expert_start_idx
recv_topk_idx.masked_fill_(~mask, -1)
关键点:
- 使用 dist.all_to_all_single(PyTorch 的 NCCL 封装)作为通信原语
- src_token_global_idx = src_rank_idx * num_max_tokens_per_rank + src_token_local_idx 的全局索引编码方式与 DeepEP 一致
- 不在本 rank 的 Expert 索引置为 -1
8.2 ref_combine —— Combine 参考实现
# refs.py:177-243
def combine(y, topk_idx, num_scaleout_ranks, num_scaleup_ranks, num_experts,
bias, reduce_in_local, reduce_in_scaleup):
三种模式:
- (True, True) → Hybrid combine(先 rank 内 reduce,再 scaleup 内 reduce)
- (True, False) → Non-hybrid combine(仅 rank 内 reduce)
- (False, False) → 无 reduce,直接 accumulate
核心是 grouped_reduce 函数,按 group_id 分组进行有序 segmented reduce:
# refs.py:201-234
def grouped_reduce(data_to_reduce, group_id):
group_id, src_indices = torch.sort(group_id, dim=-1, stable=True)
# 重排 data 使同组连续
data_to_reduce = data_to_reduce.view(-1, hidden)[transformed_src_indices]...
# 逐 topk 维度扫描,检测 segment break
for i in range(num_topk):
is_segment_break = ... group_id[:, i] != group_id[:, i + 1]
cur_accum_buf += data_to_reduce[:, i, :].float()
data_to_reduce[segment_break_token_indices, i] = cur_accum_buf[...]
cur_accum_buf[segment_break_token_indices] = 0.0
8.3 generate_pre_combine_data —— 确定性测试数据生成
# refs.py:126-153
def generate_pre_combine_data(src_token_global_idx, num_max_tokens_per_rank, num_topk, hidden):
token_seeds = (src_token_global_idx.unsqueeze(1) * num_topk +
torch.arange(num_topk, ...).unsqueeze(0))
max_seed = num_ranks * num_max_tokens_per_rank * num_topk
result = torch.sin(
(((token_seeds * 131071 % max_seed).float() + 1) / max_seed).unsqueeze(-1) *
torch.arange(1, hidden + 1, ...) +
math.sin(float(get_global_seed()))
)
return result.to(torch.bfloat16)
设计精妙之处:
1. 确定性:仅依赖 src_token_global_idx 和 global_seed,所有 rank 可独立生成
2. 唯一性:token_seeds = global_idx * num_topk + j 保证每个 (token, topk_slot) 有不同的 seed
3. 素数哈希:131071(2^17 - 1,Mersenne 素数)取模确保 seed 分布均匀
4. sin 函数:将 seed 映射到 BF16 可表示的值域
5. 可复现性:math.sin(float(get_global_seed())) 引入全局种子偏移
9. math.py —— 数学工具集
9.1 FP8 编解码
# math.py:30-56
@torch.compile(dynamic=True)
def per_token_cast_to_fp8(x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
# 1. align hidden 到 128
x_padded = torch.nn.functional.pad(x, (0, aligned_n - n), mode='constant', value=0)
# 2. 按 128 分组计算 amax
x_padded_view = x_padded.view(m, -1, 128)
x_amax = x_padded_view.abs().float().amax(dim=2).view(m, -1).clamp(1e-4)
# 3. scale = 448 / amax (FP8 E4M3 max = 448)
return (x_padded_view * (448.0 / x_amax.unsqueeze(2))).to(torch.float8_e4m3fn).view(m, aligned_n)[:, :n].contiguous(), \
(x_amax / 448.0).view(m, -1)
关键:@torch.compile(dynamic=True) 启用 TorchCompile 加速,dynamic=True 允许动态 shape。
9.2 差异计算
# math.py:5-9
def calc_diff(x: torch.Tensor, y: torch.Tensor) -> float:
x, y = x.double() + 1, y.double() + 1
denominator = (x * x + y * y).sum()
sim = 2 * (x * y).sum() / denominator
return (1 - sim).item()
这是 Cosine Similarity 的变体(加 1 避免零向量问题),用于量化两个张量的差异。返回值越接近 0 表示越相似。
10. 测试数据生成模式
10.1 随机输入生成
# test_ep.py:94-98
x = torch.randn((num_tokens, hidden), dtype=torch.bfloat16, device='cuda')
x = per_token_cast_to_fp8(x) if use_fp8_dispatch else x
bias = torch.randn((num_tokens, hidden), dtype=torch.bfloat16, device='cuda') if num_bias == 1 else None
10.2 Masked Expert Selection
# test_ep.py:78-81
if args.masked_ratio > 0:
rand_mask = torch.rand_like(topk_idx, dtype=torch.float)
topk_idx.masked_fill_(rand_mask < args.masked_ratio, -1)
topk_weights.masked_fill_(topk_idx < 0, 0)
模拟 token 未选中任何 Expert 的场景(topk_idx = -1)。
10.3 全模式枚举
# test_ep.py:22-31
def enumerate_ep_modes():
for do_handle_copy in (1, 0):
for expert_alignment in (128, 1):
for use_fp8_dispatch in (1, 0):
for num_bias in (0, 1, 2):
for with_previous_event in (0, 1):
for async_with_compute_stream in (0, 1):
for allocate_on_comm_stream in ((1, ) if with_previous_event else (0, 1)):
yield (do_handle_copy, expert_alignment, use_fp8_dispatch, num_bias,
with_previous_event, async_with_compute_stream, allocate_on_comm_stream)
2 × 2 × 2 × 3 × 2 × 2 × ~1 = ~96 种组合,覆盖所有功能交叉场景。这是组合测试的典型案例。
11. 验证模式
11.1 Bitwise Identical(主要验证方式)
# test_ep.py:503-508
assert torch.equal(combined_x, ref_combined_y), \
f'Diff: {calc_diff(combined_x, ref_combined_y)}'
assert torch.equal(reduced_combined_x, ref_reduced_combined_y), \
f'Diff: {calc_diff(reduced_combined_x, ref_reduced_combined_y)}'
assert torch.equal(combined_topk_weights, topk_weights), \
f'{calc_diff(combined_topk_weights, topk_weights)}'
torch.equal 要求逐比特一致。这是 DeepEP 测试的核心验证方式,因为:
1. Dispatch 是确定性路由 —— 给定相同输入,接收的数据应完全相同
2. Combine 使用 deterministic algorithm —— 累加顺序确定,结果唯一
3. BF16 精度下,只要计算顺序一致,结果必然 bitwise identical
11.2 差异诊断
当 bitwise 不一致时,calc_diff 提供量化诊断信息:
# test_ep.py:500
assert torch.equal(ref_t, t), f'{ref_t=}, {t=}'
11.3 专家计数验证
# test_ep.py:448-462
for i in range(num_local_experts):
ref_count = (ref_recv_topk_idx == i).sum().item()
aligned_ref_count = align(ref_count, expert_alignment)
assert ref_count == cumulative_local_expert_recv_stats[i].item()
assert aligned_ref_count == handle.num_recv_tokens_per_expert_list[i]
11.4 Zero Padding 验证
# test_ep.py:424-428
for expert_idx in range(num_local_experts):
start = expanded_handle.psum_num_recv_tokens_per_expert[expert_idx].item()
end = align(start, expert_alignment)
assert (cached_expanded_recv_x_bf16[start:end] == 0).all()
assert (cached_expanded_recv_topk_weights[start:end] == 0).all()
11.5 Deterministic 模式验证
# test_ep.py:388-397
if args.deterministic:
recv_x_twice, ... = launch(buffer, 'dispatch', ...)
assert torch.equal(recv_x_bf16, recv_x_twice_bf16)
assert torch.equal(recv_topk_idx, recv_topk_idx_twice[:num_recv_tokens])
运行两次 dispatch 并验证结果完全一致,确保 deterministic 算法的可靠性。
12. 性能度量模式
12.1 带宽计算
# test_ep.py:253-255
num_bytes_per_dispatch_token = safe_div(count_bytes(recv_x, recv_topk_idx, recv_topk_weights), recv_topk_idx.size(0))
num_scaleup_bytes = num_bytes_per_dispatch_token * num_scaleup_recv_tokens
num_scaleout_bytes = num_bytes_per_dispatch_token * num_scaleout_send_tokens
12.2 吞吐量报告
# test_ep.py:259-263
dist_print(f' * EP: {buffer.rank_idx:3}/{buffer.num_ranks} | '
f'dispatch: '
f'{num_scaleout_bytes / t / 1e9:.0f} GB/s (SO), '
f'{num_scaleup_bytes / t / 1e9:.0f} GB/s (SU), {t * 1e6:.3f} us, {num_scaleup_bytes:.0f} bytes | '
f'copy: {2 * num_recv_tokens * num_bytes_per_dispatch_token / copy_t / 1e9:.0f} GB/s, {copy_t * 1e6:.3f} us')
报告维度:
- Scale-Out GB/s:跨节点 RDMA 吞吐
- Scale-Up GB/s:节点内 NVLink 吞吐
- 延迟 (μs)
- Copy 阶段带宽
- 总字节数
12.3 流量分析
测试框架精确计算每个 rank 的发送/接收 token 数,区分 scaleout 和 scaleup 流量:
# test_ep.py:240-245
dst_scaleout_rank_idx = topk_idx // (num_experts // num_scaleout_ranks)
num_scaleout_send_tokens = 0
for i in range(num_scaleout_ranks if num_scaleout_ranks > 1 else 0):
if args.ignore_local_traffic and i == dist.get_rank() // num_scaleup_ranks:
continue
num_scaleout_send_tokens += (dst_scaleout_rank_idx == i).any(dim=1).sum().item()
13. 其他工具
13.1 semantic.py —— weak_lru
# semantic.py:9-27
def weak_lru(maxsize: Optional[int] = 128, typed: bool = False):
def wrapper(func):
@functools.lru_cache(maxsize, typed)
def _func(_self, *args, **kwargs):
return func(_self(), *args, **kwargs)
@functools.wraps(func)
def inner(self, *args, **kwargs):
return _func(weakref.ref(self), *args, **kwargs)
return inner
return wrapper
使用 weakref.ref(self) 作为 LRU cache 的键,避免 lru_cache 持有 self 的强引用导致内存泄漏。适用于需要缓存方法的场景。
13.2 find_pkgs.py —— NVIDIA 包路径探测
# find_pkgs.py:8-54
def find_pkg_root(name: str, lib_name: Optional[str] = None, optional: bool = False):
# 1. 检查环境变量 EP_{NAME}_ROOT_DIR 和 {NAME}_DIR
# 2. 遍历 importlib.metadata.distributions()
# 3. 匹配 nvidia-{name} 包名
# 4. 查找 lib_name 文件确定 root
用于在构建 / 运行时定位 NCCL、NVSHMEM 等 NVIDIA 库的安装路径。
14. 设计哲学总结
14.1 "Bitwise or Nothing"
DeepEP 测试不采用误差容忍(atol/rtol)比较,而是要求 bitwise identical。这之所以可行,是因为:
1. 所有计算在 BF16 下是确定性的(无 TF32 等自动精度提升)
2. 使用 deterministic_sort 保证累加顺序一致
3. 通信是 all_to_all 的确定性置换
14.2 全模式覆盖
enumerate_ep_modes 枚举所有功能开关的组合,确保新功能不会在某个组合下回归。
14.3 硬件感知
get_nvlink_gbs、get_rdma_gbs、check_fast_rdma_atomic_support 使测试能感知硬件能力,据此调整性能预期或启用/禁用特定优化路径。
14.4 参考实现驱动
所有正确性验证基于独立的 PyTorch 参考实现(refs.py),而非手工计算的期望值。这保证了:
- 参考实现易于理解和验证
- 任何协议变更只需修改参考实现
- GPU kernel 与参考实现对齐
14.5 生产级 Benchmark
bench_kineto 不是简单的 time.time() 测量,而是:
- 使用 PyTorch profiler 获取真实 kernel 时间
- 消除 CPU 发射不均(barrier_comm_profiling)
- 消除缓存效应(flush_l2_cache)
- 支持多 kernel 周期分析(num_kernels_per_period)
15. 关键文件行数统计
| 文件 | 行数 | 核心职责 |
|---|---|---|
tests/elastic/test_ep.py |
610 | 集成测试主入口 |
deep_ep/utils/testing.py |
220 | Benchmark 工具 |
deep_ep/utils/gate.py |
181 | 非均衡流量生成 |
deep_ep/utils/comm.py |
84 | NCCL Comm 封装 |
deep_ep/utils/envs.py |
269 | 环境初始化 / 硬件探测 |
deep_ep/utils/event.py |
97 | CUDA Event 封装 |
deep_ep/utils/refs.py |
244 | PyTorch 参考实现 |
deep_ep/utils/math.py |
104 | FP8 / 数学工具 |
deep_ep/utils/semantic.py |
28 | weak_lru 等 |
deep_ep/utils/find_pkgs.py |
83 | 包路径探测 |
| 合计 | ~1920 | — |
附录:测试运行命令示例
# 基本正确性测试 (8 卡)
torchrun --nproc_per_node=8 tests/elastic/test_ep.py --num-tokens 4096 --hidden 7168 --num-experts 256 --num-topk 6
# 跳过正确性验证,仅做性能测试
torchrun --nproc_per_node=8 tests/elastic/test_ep.py --skip-check
# 压力测试 (循环多种子)
torchrun --nproc_per_node=8 tests/elastic/test_ep.py --do-pressure-test
# 非均衡流量测试
torchrun --nproc_per_node=8 tests/elastic/test_ep.py --unbalanced-ratio 2.0
# Deterministic 模式
torchrun --nproc_per_node=8 tests/elastic/test_ep.py --deterministic
# 导出 profiling trace
torchrun --nproc_per_node=8 tests/elastic/test_ep.py --dump-profile-traces ./traces