minimind/scripts/analyze_profile.py

198 lines
8.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"""
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()