198 lines
8.4 KiB
Python
198 lines
8.4 KiB
Python
"""
|
||
Parse torch.profiler chrome trace, summarize top ops + backend detection.
|
||
|
||
Usage:
|
||
# trace_out/ 是 train_pretrain.py --profile_out 指定的目录
|
||
python analyze_profile.py --trace_dir ../profile_out
|
||
|
||
# 或指定具体 trace 文件(一个 rank 一个)
|
||
python analyze_profile.py --trace_file ../profile_out/DEVvm54331_XXX.pt.trace.json
|
||
"""
|
||
import argparse
|
||
import glob
|
||
import gzip
|
||
import json
|
||
import os
|
||
from collections import defaultdict
|
||
|
||
|
||
# op 名 → bucket 分类规则(按前缀 / substring 匹配)
|
||
BUCKET_RULES = [
|
||
('matmul (GEMM)', ['aten::mm', 'aten::bmm', 'aten::addmm', 'aten::linear']),
|
||
('attention (SDPA/FA)', ['flash', 'scaled_dot_product', 'sdpa', 'attention',
|
||
'aten::_scaled_dot_product', 'FlashAttn', 'mem_efficient',
|
||
'triton_flash', 'ampere_fp16_s16816gemm_bf16']),
|
||
('elementwise (norm/act)', ['rms_norm', 'RMSNorm', 'aten::silu', 'aten::mul', 'aten::add',
|
||
'aten::div', 'aten::rsqrt', 'aten::sub', 'element_wise']),
|
||
('softmax', ['softmax', 'log_softmax', 'cross_entropy']),
|
||
('rope', ['rope', 'rotary', 'cos', 'sin']),
|
||
('embedding', ['embedding', 'aten::embedding']),
|
||
('dropout', ['dropout']),
|
||
('reduce (all-reduce/gather)', ['nccl', 'ncclAllReduce', 'ncclAllGather', 'ncclReduceScatter',
|
||
'allreduce', 'AllReduce', 'ProcessGroupNCCL']),
|
||
('memory (copy/permute/view)', ['aten::to', 'aten::_to_copy', 'aten::contiguous',
|
||
'aten::permute', 'aten::view', 'aten::reshape',
|
||
'aten::transpose', 'aten::clone', 'Memcpy']),
|
||
('optimizer', ['adam', 'AdamW', 'optim', 'clip_grad']),
|
||
]
|
||
|
||
# attention backend 识别(chrome trace event name)
|
||
SDPA_BACKENDS = {
|
||
'flash': ['FlashAttn', 'flash_attn', 'triton_flash', 'ampere_fp16_s16816gemm'],
|
||
'mem_efficient': ['mem_efficient', 'MemEfficient', 'cutlassF'],
|
||
'math': ['aten::baddbmm', 'aten::softmax'], # naive fallback
|
||
}
|
||
|
||
|
||
def bucket_op(name: str) -> str:
|
||
ln = name.lower()
|
||
for bucket, patterns in BUCKET_RULES:
|
||
for p in patterns:
|
||
if p.lower() in ln:
|
||
return bucket
|
||
return 'other'
|
||
|
||
|
||
def load_trace(path):
|
||
"""支持 .json 和 .json.gz。"""
|
||
opener = gzip.open if path.endswith('.gz') else open
|
||
with opener(path, 'rt') as f:
|
||
data = json.load(f)
|
||
return data.get('traceEvents', data)
|
||
|
||
|
||
def analyze(events):
|
||
# 只统计 CUDA kernel 时间(cat == 'kernel' 或 'cuda_runtime')
|
||
# 但 torch.profiler 输出的 chrome trace 里 CUDA op 是 name+dur 的 complete event (ph=='X')
|
||
stats = defaultdict(lambda: {'time_us': 0, 'count': 0})
|
||
total_cuda_us = 0
|
||
sdpa_backend_hits = defaultdict(int)
|
||
|
||
for e in events:
|
||
if e.get('ph') != 'X':
|
||
continue
|
||
cat = e.get('cat', '')
|
||
name = e.get('name', '')
|
||
dur = e.get('dur', 0)
|
||
# 只关心 GPU kernel + collective + cudaLaunch,跳过 python-level
|
||
if cat not in ('kernel', 'cuda_runtime', 'user_annotation', 'gpu_memcpy', 'gpu_memset', 'cpu_op'):
|
||
continue
|
||
if cat == 'cpu_op' and 'ncclAllReduce' not in name and 'sync' not in name.lower():
|
||
# CPU op 大部分是 dispatch overhead,除了 NCCL / sync 都跳
|
||
continue
|
||
stats[name]['time_us'] += dur
|
||
stats[name]['count'] += 1
|
||
if cat in ('kernel', 'gpu_memcpy', 'gpu_memset'):
|
||
total_cuda_us += dur
|
||
|
||
# 探测 attention backend
|
||
for backend, patterns in SDPA_BACKENDS.items():
|
||
for p in patterns:
|
||
if p in name:
|
||
sdpa_backend_hits[backend] += dur
|
||
|
||
return stats, total_cuda_us, sdpa_backend_hits
|
||
|
||
|
||
def format_us(us):
|
||
if us > 1e6: return f'{us/1e6:.2f} s'
|
||
if us > 1e3: return f'{us/1e3:.2f} ms'
|
||
return f'{us:.0f} us'
|
||
|
||
|
||
def report(stats, total_cuda_us, sdpa_backend_hits, top_n=25):
|
||
print(f'\n{"=" * 80}')
|
||
print(f' Total CUDA time in trace: {format_us(total_cuda_us)}')
|
||
print(f'{"=" * 80}')
|
||
|
||
# bucket 汇总
|
||
bucket_time = defaultdict(int)
|
||
bucket_count = defaultdict(int)
|
||
for name, s in stats.items():
|
||
b = bucket_op(name)
|
||
bucket_time[b] += s['time_us']
|
||
bucket_count[b] += s['count']
|
||
|
||
print(f'\n Time by bucket (% of total CUDA):')
|
||
print(f' {"bucket":40s} {"time":>12s} {"% total":>8s} {"# calls":>8s}')
|
||
for bucket, t in sorted(bucket_time.items(), key=lambda kv: -kv[1]):
|
||
pct = t / max(total_cuda_us, 1) * 100
|
||
print(f' {bucket:40s} {format_us(t):>12s} {pct:>7.1f}% {bucket_count[bucket]:>8d}')
|
||
|
||
# attention backend 探测
|
||
print(f'\n Attention backend detection:')
|
||
if sdpa_backend_hits:
|
||
winner = max(sdpa_backend_hits.items(), key=lambda kv: kv[1])
|
||
for backend, t in sorted(sdpa_backend_hits.items(), key=lambda kv: -kv[1]):
|
||
marker = ' ← DOMINANT' if backend == winner[0] else ''
|
||
print(f' {backend:20s}: {format_us(t):>12s}{marker}')
|
||
if winner[0] != 'flash':
|
||
print(f' ⚠️ Not using FlashAttention. Expected FA2 on A100 for hidden 128/heads 16 → suboptimal.')
|
||
print(f' Fix: 训练里用 `torch.nn.attention.sdpa_kernel(SDPBackend.FLASH_ATTENTION)` context')
|
||
else:
|
||
print(f' (未识别到 attention kernel——可能 kernel 名 encoded 或未走 SDPA)')
|
||
|
||
# top ops 明细
|
||
print(f'\n Top {top_n} CUDA ops:')
|
||
print(f' {"rank":>4s} {"time":>12s} {"% total":>8s} {"# calls":>8s} {"name":s}')
|
||
sorted_ops = sorted(stats.items(), key=lambda kv: -kv[1]['time_us'])
|
||
for i, (name, s) in enumerate(sorted_ops[:top_n], 1):
|
||
pct = s['time_us'] / max(total_cuda_us, 1) * 100
|
||
# 截断长 kernel 名
|
||
display_name = name if len(name) < 80 else name[:77] + '...'
|
||
print(f' {i:>4d} {format_us(s["time_us"]):>12s} {pct:>7.1f}% {s["count"]:>8d} {display_name}')
|
||
|
||
# 分类建议
|
||
print(f'\n{"=" * 80}')
|
||
print(f' 优化提示(按 bucket 比例):')
|
||
print(f'{"=" * 80}')
|
||
total = total_cuda_us
|
||
reduce_pct = bucket_time.get('reduce (all-reduce/gather)', 0) / max(total, 1) * 100
|
||
if reduce_pct > 15:
|
||
print(f' ⚠️ reduce/collective 占 {reduce_pct:.1f}%(>15% 算高)→ 考虑:')
|
||
print(f' - `no_sync()` 包 gradient accumulation 减少 3× 冗余 all-reduce')
|
||
print(f' - FSDP: 用 ZeRO-2/3 拆 optimizer state 减少每次 sync 数据量')
|
||
elem_pct = bucket_time.get('elementwise (norm/act)', 0) / max(total, 1) * 100
|
||
if elem_pct > 15:
|
||
print(f' ⚠️ elementwise 占 {elem_pct:.1f}%(>15% 算高)→ memory bandwidth bound')
|
||
print(f' - Fused RMSNorm + SiLU + Add kernel(apex 或 triton)省 50%+')
|
||
mem_pct = bucket_time.get('memory (copy/permute/view)', 0) / max(total, 1) * 100
|
||
if mem_pct > 8:
|
||
print(f' ⚠️ memory movement 占 {mem_pct:.1f}%(>8% 算高)→ 布局问题')
|
||
print(f' - 检查 permute/transpose/contiguous 频次,可能重复布局转换')
|
||
attn_pct = bucket_time.get('attention (SDPA/FA)', 0) / max(total, 1) * 100
|
||
matmul_pct = bucket_time.get('matmul (GEMM)', 0) / max(total, 1) * 100
|
||
print(f'\n Matmul + Attention (核心 compute): {matmul_pct + attn_pct:.1f}%')
|
||
print(f' 如果这个数 <50%,多数时间在做非核心 op → 有巨大优化空间')
|
||
print(f' Llama 3 训练里这个数一般 60-75%')
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser()
|
||
parser.add_argument('--trace_file', type=str, default=None)
|
||
parser.add_argument('--trace_dir', type=str, default='../profile_out')
|
||
parser.add_argument('--top_n', type=int, default=25)
|
||
args = parser.parse_args()
|
||
|
||
if args.trace_file:
|
||
files = [args.trace_file]
|
||
else:
|
||
# torch.profiler 每 rank 一个文件;分析 rank 0 即可
|
||
files = sorted(glob.glob(os.path.join(args.trace_dir, '*.pt.trace.json*')))
|
||
if not files:
|
||
print(f'no trace file found in {args.trace_dir}')
|
||
return
|
||
# 只取第一个(一般 rank 0)
|
||
files = files[:1]
|
||
|
||
for f in files:
|
||
print(f'\n[loading] {f} ({os.path.getsize(f)/1e6:.1f} MB)')
|
||
events = load_trace(f)
|
||
print(f'[loaded] {len(events):,} events')
|
||
stats, total, sdpa_hits = analyze(events)
|
||
report(stats, total, sdpa_hits, top_n=args.top_n)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|