profile_8020_pipeline.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 8020 识别链路分段计时(249 上跑,不改生产代码,monkey-patch 打点)。
  5. 阶段划分(对应 query_all 一次调用):
  6. read 读图 Image.open+np.array
  7. yolo YOLO predict(含 letterbox/NMS,GPU)
  8. post mask 二值化+质心+合并(detect_and_crop_all 总时 - yolo - crop)
  9. crop 逐实例 裁卡+warp(CPU)
  10. letterbox 裁卡图 letterbox392 + split_half(CPU,每卡)
  11. dino_u 上半区 DINOv2 前向→L2→cpu(GPU,每卡)
  12. dino_l 下半区 DINOv2 前向→L2→cpu(GPU,每卡)
  13. milvus_s milvus upper_coll.search(网络召回K=30,每卡)
  14. milvus_q milvus lower_coll.query(网络拉30条向量,每卡)
  15. rerank 下半区 numpy dot + fusion 排序(CPU,每卡;= milvus总 - s - q + rank)
  16. total query_all 端到端
  17. GPU 段天然有 .cpu().numpy() 同步点,perf_counter 直接可用。
  18. 每阶段记每次调用的 ms;输出 mean/p50 与占总时比例。
  19. 用法(pytorch env,249):
  20. CUDA_DEVICE_ORDER=PCI_BUS_ID CUDA_VISIBLE_DEVICES=1 \
  21. ~/miniconda3/envs/pytorch/bin/python tools/profile_8020_pipeline.py \
  22. --images data/audit_imgs/raspi_front_2026-09-01_13-22-48_529284.jpg \
  23. data/audit_imgs/multicard_2up.jpg --n 15
  24. """
  25. import os
  26. import sys
  27. import time
  28. import argparse
  29. import statistics
  30. sys.stdout.reconfigure(encoding="utf-8")
  31. _ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  32. sys.path.insert(0, _ROOT)
  33. sys.path.insert(0, os.path.join(_ROOT, "scripts"))
  34. import numpy as np
  35. from PIL import Image
  36. import config
  37. from scripts.cascade_match import DualCascadeMatcher
  38. import scripts.cascade_match as cm
  39. # 阶段 → [每次调用ms]
  40. T = {}
  41. def _rec(key, ms):
  42. T.setdefault(key, []).append(ms)
  43. def _wrap(obj, attr, key, pre=None):
  44. """给 obj.attr 方法包一层计时;pre=objself 时传入宿主对象。"""
  45. fn = getattr(obj, attr)
  46. if getattr(fn, "_profile_wrapped", False):
  47. return
  48. def wrapper(*a, **kw):
  49. t0 = time.perf_counter()
  50. try:
  51. return fn(*a, **kw)
  52. finally:
  53. _rec(key, (time.perf_counter() - t0) * 1000)
  54. wrapper._profile_wrapped = True
  55. setattr(obj, attr, wrapper)
  56. def install(matcher):
  57. # YOLO predict 全程
  58. _wrap(matcher.detector, "_predict", "yolo")
  59. # 裁卡+warp(每实例)
  60. _wrap(matcher.detector, "_crop_one_instance", "crop")
  61. # letterbox + split_half(cascade_match 命名空间里的引用,每卡一次)
  62. _wrap(cm, "letterbox392", "letterbox")
  63. _wrap(cm, "split_half", "split_half")
  64. # DINOv2 上/下区前向(每卡一次)
  65. _wrap(matcher.extractor, "extract_upper", "dino_u")
  66. _wrap(matcher.extractor, "extract_lower", "dino_l")
  67. # milvus 两次网络往返(每卡)——backend=milvus 时存在
  68. if hasattr(matcher, "upper_coll"):
  69. _wrap(matcher.upper_coll, "search", "milvus_s")
  70. _wrap(matcher.lower_coll, "query", "milvus_q")
  71. # gpu 常驻图库召回(backend=gpu 时存在)
  72. if hasattr(matcher, "gallery_upper"):
  73. _wrap(matcher, "_query_raw_gpu", "gpu_recall")
  74. # fusion 重排
  75. _wrap(matcher, "rank", "rank")
  76. def run_once(matcher, path):
  77. t0 = time.perf_counter()
  78. matcher.query_all(path)
  79. total = (time.perf_counter() - t0) * 1000
  80. _rec("total", total)
  81. _rec("read", 0.0) # 占位;read 实测并入下方独立测量
  82. def main():
  83. ap = argparse.ArgumentParser()
  84. ap.add_argument("--images", nargs="+", required=True)
  85. ap.add_argument("--n", type=int, default=15, help="正式计时次数(另有3次warmup)")
  86. ap.add_argument("--device", default="cuda:0")
  87. args = ap.parse_args()
  88. print(f"[Profile] 初始化 matcher(device={args.device},与 8020 共存仅占显存不抢算力)...", flush=True)
  89. matcher = DualCascadeMatcher(
  90. alpha=config.CASCADE_ALPHA, top_k_recall=config.CASCADE_TOP_K_RECALL,
  91. device=__import__("torch").device(args.device),
  92. detector_device=__import__("torch").device(args.device),
  93. milvus_alias="prof", verbose=True,
  94. )
  95. install(matcher)
  96. # read 阶段单独测(query_all 内部的 Image.open 复刻)
  97. def measure_read(path, n):
  98. for _ in range(n):
  99. t0 = time.perf_counter()
  100. np.array(Image.open(path).convert("RGB"))
  101. _rec("read", (time.perf_counter() - t0) * 1000)
  102. ORDER = ["total", "read", "yolo", "post", "crop", "letterbox", "split_half",
  103. "dino_u", "dino_l", "milvus_s", "milvus_q", "gpu_recall", "rank"]
  104. for path in args.images:
  105. T.clear()
  106. for _ in range(3):
  107. run_once(matcher, path)
  108. T.clear() # warmup 丢弃
  109. for _ in range(args.n):
  110. run_once(matcher, path)
  111. measure_read(path, args.n)
  112. n_cards = len(matcher.query_all(path)) # 最终跑一次看卡数(计时已污染,仅取数)
  113. print(f"\n===== {os.path.basename(path)} 卡数={n_cards} N={args.n} =====", flush=True)
  114. print(f"{'阶段':<12}{'mean ms':>9}{'p50 ms':>9}{'每次请求次数':>10}{'占总时':>8}", flush=True)
  115. total_mean = statistics.mean(T.get("total", [1]))
  116. rows = {}
  117. for k in ORDER:
  118. v = T.get(k)
  119. if not v:
  120. continue
  121. mean = statistics.mean(v)
  122. rows[k] = mean
  123. per_req = len(v) / max(1, args.n)
  124. print(f"{k:<12}{mean:>9.1f}{statistics.median(v):>9.1f}{per_req:>10.1f}"
  125. f"{mean / total_mean:>7.1%}", flush=True)
  126. # post = detect_and_crop_all 整体由 yolo+crop+post 组成,post 无直接打点 → 用差值估
  127. # (detect_and_crop_all 未单独包计时,此处不拆,yolo/crop 已覆盖大头)
  128. gpu_sum = sum(rows.get(k, 0) for k in ("yolo", "dino_u", "dino_l", "gpu_recall"))
  129. cpu_sum = sum(rows.get(k, 0) for k in ("read", "post", "crop", "letterbox", "split_half", "rank"))
  130. io_sum = sum(rows.get(k, 0) for k in ("milvus_s", "milvus_q"))
  131. print(f"[归类] GPU(yolo+dino+召回)≈{gpu_sum:.0f}ms CPU(读图/裁卡/letterbox/rank)≈{cpu_sum:.0f}ms "
  132. f"Milvus IO≈{io_sum:.0f}ms (每卡请求;总={total_mean:.0f}ms)", flush=True)
  133. print("\n[说明] post=mask二值化+质心+合并,未单独打点;total 与各段之和的差值即它+同步开销。"
  134. "\n[说明] dino_u/dino_l 为每卡一次:双卡图时每请求调 2 次,表内 mean 为单次调用耗时。", flush=True)
  135. if __name__ == "__main__":
  136. main()