""" 将 PyTorch CUDA profiler 事件按内核/算子名称粗分为若干类,便于估算 memcpy、GEMM/Linear、Attention、Norm 等在 GPU 时间中的占比。 说明: - 分类基于名称子串启发式,不同 CUDA / Triton / cuBLAS 版本下内核名会有差异; - 若需更细粒度,请配合 prof.export_chrome_trace() 在 chrome://tracing 或 Perfetto 中查看。 """ from __future__ import annotations from collections import defaultdict from typing import DefaultDict, Dict, Iterable, List, Tuple # 顺序有意义:先匹配更具体的类别 _BUCKET_ORDER: List[str] = [ "memcpy_sync", "attention", "gemm_linear", "conv", "norm", "elementwise_reduce", "other", ] def classify_cuda_name(name: str) -> str: """根据内核或 aten 算子名称归入粗分类。""" n = (name or "").lower() # 设备同步、拷贝、显存设置(含部分 launch 开销) if any( s in n for s in ( "memcpy", "memset", "memgetinfo", "cudaget", "cudaevent", "cudaoccupancy", "cuda stream", "cudastream", "cudadevicesynchronize", "cuda devicesynchronize", "device synchronize", "eventrecord", "eventsynchronize", ) ): return "memcpy_sync" # FlashAttention / SDPA / 各类 fused attention if any( s in n for s in ( "flash_attn", "flash-attn", "scaled_dot_product", "sdpa", "efficient_attention", "mem_eff_attention", "fused_attention", "fmha", "mha_default", "multi_head_attention", "contrib_attn", "attention_forward", "attention_backward", "softmax", ) ): # 独立 softmax 小核也可能与 attention 同桶;若需拆开可把 softmax 挪到 elementwise return "attention" # MatMul / Linear / GEMM(含 cutlass、cublas、triton、fp8) if any( s in n for s in ( "gemm", "cublas", "cutlass", "matmul", "mat_mul", "aten::linear", "aten::mm", "aten::bmm", "aten::addmm", "aten::matmul", "scaled_mm", "fp8", "wmma", "mma_sync", "triton", "dot", ) ): return "gemm_linear" if any(s in n for s in ("conv", "cudnn", "depthwise", "convolution")): return "conv" if any( s in n for s in ( "layernorm", "layer_norm", "rms_norm", "group_norm", "flash_norm", "aten::layer_norm", "aten::group_norm", "aten::native_layer_norm", ) ): return "norm" if any( s in n for s in ( "elementwise", "vectorized", "unary", "binary", "reduce", "reduction", "activation", "silu", "gelu", "swiglu", "relu", "aten::add", "aten::mul", "aten::div", "aten::pow", "aten::sqrt", "aten::rsqrt", ) ): return "elementwise_reduce" return "other" def _event_cuda_time_us(event) -> float: for attr in ( "cuda_time_total", "self_cuda_time_total", "self_cuda_time_total_us", ): v = getattr(event, attr, None) if v is not None: return float(v) return 0.0 def aggregate_from_profiler(prof) -> Tuple[Dict[str, float], List[Tuple[str, float]]]: """ 从 torch.profiler.profile 实例聚合: - 返回 (bucket -> 微秒总和, 按耗时排序的 (name, us) 列表) 使用 prof.events() 以尽量接近 CUDA 内核级名称。 """ bucket_us: DefaultDict[str, float] = defaultdict(float) per_name: DefaultDict[str, float] = defaultdict(float) try: events = prof.events() except Exception: events = [] for e in events: us = _event_cuda_time_us(e) if us <= 0: continue name = getattr(e, "name", "") or "" b = classify_cuda_name(name) bucket_us[b] += us per_name[name] += us # 若 events() 为空,回退到 key_averages(多为 aten 级) if not bucket_us and not per_name: try: for avg in prof.key_averages(): us = float(getattr(avg, "cuda_time_total", 0) or getattr(avg, "self_cuda_time_total", 0) or 0) if us <= 0: continue name = getattr(avg, "key", "") or "" b = classify_cuda_name(name) bucket_us[b] += us per_name[name] += us except Exception: pass sorted_names = sorted(per_name.items(), key=lambda x: -x[1]) out = {k: bucket_us.get(k, 0.0) for k in _BUCKET_ORDER} for k, v in bucket_us.items(): if k not in out: out[k] = v return out, sorted_names def format_bucket_report( bucket_us: Dict[str, float], top_names: Iterable[Tuple[str, float]], top_k: int = 25, ) -> str: total = sum(bucket_us.values()) or 1e-9 lines: List[str] = [] lines.append("=== CUDA 时间按粗分类(微秒 / 占比)===") for k in _BUCKET_ORDER: if k in bucket_us and bucket_us[k] > 0: us = bucket_us[k] lines.append(f" {k:22s} {us:12.1f} us ({100.0 * us / total:5.1f}%)") # 其它未在顺序表中的桶 for k, us in sorted(bucket_us.items(), key=lambda x: -x[1]): if k in _BUCKET_ORDER: continue if us > 0: lines.append(f" {k:22s} {us:12.1f} us ({100.0 * us / total:5.1f}%)") lines.append(f" {'TOTAL':22s} {total:12.1f} us") lines.append("") lines.append(f"=== 耗时最高的 {top_k} 个事件名(微秒)===") for i, (name, us) in enumerate(top_names): if i >= top_k: break short = name if len(name) <= 120 else name[:117] + "..." lines.append(f" {us:10.1f} {short}") return "\n".join(lines)