analyze_steps.py 4.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
  1. import gzip, json, sys, collections, statistics
  2. path=sys.argv[1]; dump_step=int(sys.argv[2]) if len(sys.argv)>2 else -1
  3. with gzip.open(path,'rt') as f: data=json.load(f)
  4. ev=[e for e in data['traceEvents'] if e.get('ph')=='X']
  5. kern=sorted([e for e in ev if e.get('cat') in ('kernel','gpu_memcpy','gpu_memset')],key=lambda e:e['ts'])
  6. launches=sorted([e for e in ev if e.get('cat')=='cuda_runtime' and 'GraphLaunch' in e['name']],key=lambda e:e['ts'])
  7. # steps: pair consecutive graph launches (2 per decode step). Use the GPU kernels between launch[i] start and launch[i+2] start.
  8. starts=[l['ts'] for l in launches[0::2]]
  9. ends=starts[1:]+[max(e['ts']+e['dur'] for e in kern)]
  10. def cat_of(n):
  11. l=n.lower()
  12. if 'nccl' in l: return 'nccl'
  13. if 'memcpy' in l or 'memset' in l: return 'memcpy'
  14. if 'w8a8_block_fp8' in l: return 'fp8_triton_gemm'
  15. if 'groupproblemshape' in l: return 'moe_grouped_gemm'
  16. if 'cutlass_80_wmma' in l or 'nvjet' in l or 'gemm' in l and 'tiny' not in l: return 'bf16_gemm'
  17. if 'tiny_n_gemm' in l: return 'router_tiny_gemm'
  18. if 'hc_' in l or 'mhc' in l: return 'hyperconn'
  19. if 'sparse_mla' in l or 'mqa_logits' in l or 'topk' in l or 'page_' in l or 'rope' in l or 'k_norm' in l: return 'attention'
  20. if 'engram' in l or 'hash' in l: return 'engram'
  21. if 'tensorrt_llm' in l or 'moe' in l or 'expert' in l or 'router' in l or 'silu' in l or 'quantize_with_block' in l: return 'moe_aux'
  22. if 'quant' in l: return 'quant'
  23. if 'norm' in l: return 'norm'
  24. return 'other'
  25. per=[]
  26. for i,(s,e) in enumerate(zip(starts,ends)):
  27. ks=[k for k in kern if s<=k['ts']<e]
  28. if not ks: continue
  29. d=collections.defaultdict(float); c=collections.Counter()
  30. for k in ks: d[cat_of(k['name'])]+=k['dur']; c[cat_of(k['name'])]+=1
  31. busy=0;cs=None;ce=None
  32. for k in ks:
  33. a,b=k['ts'],k['ts']+k['dur']
  34. if ce is None or a>ce:
  35. if ce is not None: busy+=ce-cs
  36. cs,ce=a,b
  37. else: ce=max(ce,b)
  38. busy+=ce-cs
  39. per.append(dict(i=i,wall=e-s,busy=busy,cats=d,cnt=c,n=len(ks)))
  40. per=per[1:-1] # drop first (may include prefill tail) and last (truncated)
  41. print(f"decode steps analysed: {len(per)}")
  42. walls=[p['wall']/1000 for p in per]; print(f"step wall ms: mean={statistics.mean(walls):.1f} median={statistics.median(walls):.1f} min={min(walls):.1f} max={max(walls):.1f}")
  43. print(f"GPU busy per step: mean={statistics.mean(p['busy']/1000 for p in per):.1f} ms")
  44. allc=collections.defaultdict(list)
  45. for p in per:
  46. for k in set().union(*[q['cats'].keys() for q in per]): allc[k].append(p['cats'].get(k,0)/1000)
  47. print("\n== per-step GPU time by category (ms): mean / median / max ; kernels per step ==")
  48. tot=0
  49. for k,v in sorted(allc.items(),key=lambda kv:-statistics.mean(kv[1])):
  50. n=statistics.mean(p['cnt'].get(k,0) for p in per); tot+=statistics.mean(v)
  51. print(f" {k:18s} {statistics.mean(v):7.1f} / {statistics.median(v):7.1f} / {max(v):7.1f} n={n:.0f}")
  52. print(f" {'SUM':18s} {tot:7.1f}")
  53. # NCCL per-step
  54. nc=[p['cats'].get('nccl',0)/1000 for p in per]
  55. print(f"\nNCCL per step ms: mean={statistics.mean(nc):.1f} median={statistics.median(nc):.1f} min={min(nc):.1f} max={max(nc):.1f}")
  56. nk=[k for k in kern if 'nccl' in k['name'].lower() and k['ts']>=starts[1]]
  57. ds=sorted(k['dur'] for k in nk)
  58. import bisect
  59. print("NCCL kernel duration percentiles us: " + " ".join(f"p{q}={ds[min(len(ds)-1,int(len(ds)*q/100))]:.0f}" for q in (10,25,50,75,90,95,99)))
  60. print(f"NCCL kernels <150us: {sum(1 for x in ds if x<150)/len(ds)*100:.0f}% time share of >300us kernels: {sum(x for x in ds if x>300)/sum(ds)*100:.0f}%")
  61. if dump_step>=0:
  62. p=per[dump_step]; s=starts[p['i']]; e=ends[p['i']]
  63. ks=[k for k in kern if s<=k['ts']<e]
  64. print(f"\n== kernel sequence for step {p['i']} (collapsed runs), wall={p['wall']/1000:.1f}ms ==")
  65. runs=[]
  66. for k in ks:
  67. nm=k['name'][:70]
  68. if runs and runs[-1][0]==nm: runs[-1][1]+=1; runs[-1][2]+=k['dur']
  69. else: runs.append([nm,1,k['dur'],k['ts']-s])
  70. for nm,n,d,off in runs[:400]:
  71. print(f" +{off/1000:7.2f}ms {d:8.0f}us x{n:<3d} {nm}")