Skip to content

Commit 196cfd9

Browse files
committed
lazy-load ultralytics to reduce cold-start import time
1 parent 2099598 commit 196cfd9

6 files changed

Lines changed: 192 additions & 49 deletions

File tree

-341 KB
Loading
-317 KB
Loading

evaluation_function/evaluation.py

Lines changed: 92 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from __future__ import annotations
22

33
import os
4+
import json
5+
import time
46
import traceback
5-
from typing import Any, List, Tuple, Optional
7+
from typing import Any, List, Tuple, Optional, Callable
68
from urllib.parse import urlparse, unquote
79

810
import cv2
@@ -12,6 +14,13 @@
1214
from lf_toolkit.evaluation import Result, Params
1315

1416

17+
# Lazy loading
18+
from evaluation_function.lazy_load import LazyModule
19+
20+
torch = LazyModule("torch")
21+
ultralytics = LazyModule("ultralytics")
22+
_MODULE_IMPORT_T0 = time.perf_counter()
23+
1524
def _pget(params: Params, key: str, default: Any) -> Any:
1625
try:
1726
return params.get(key, default) # type: ignore
@@ -44,6 +53,14 @@ def _result(is_correct: bool, items: List[Tuple[str, str]]) -> Result:
4453
return Result(is_correct=is_correct, feedback_items=items)
4554

4655

56+
def _timeit(fn: Callable[[], Any]) -> Tuple[Any, float]:
57+
"""Measure wall time of fn() using perf_counter()."""
58+
t0 = time.perf_counter()
59+
out = fn()
60+
dt = time.perf_counter() - t0
61+
return out, dt
62+
63+
4764
def file_url_to_local_path(url: str) -> str:
4865
parsed = urlparse(url)
4966
path = unquote(parsed.path)
@@ -78,8 +95,7 @@ def _load_bgr_image_from_url(url: str, timeout: int = 15) -> Tuple[Optional[np.n
7895
return None, str(e)
7996

8097

81-
# ---------- NEW: ABCDE helpers (minimal & safe) ----------
82-
98+
# ABCDE helpers (minimal & safe)
8399
def _candidate_model_paths() -> List[str]:
84100
"""
85101
Matches your repo layout:
@@ -98,6 +114,13 @@ def _candidate_model_paths() -> List[str]:
98114
]
99115

100116

117+
def _add_common_timing(items: List[Tuple[str, str]], t_handler0: float) -> None:
118+
"""Append common timing fields."""
119+
now = time.perf_counter()
120+
items.append(("t_module_import_to_handler_s", f"{now - _MODULE_IMPORT_T0:.4f}"))
121+
items.append(("t_handler_elapsed_s", f"{now - t_handler0:.4f}"))
122+
123+
101124
def evaluation_function(response: Any, answer: Any, params: Params) -> Result:
102125
"""
103126
Smoke-test base + ABCDE diagnostics.
@@ -109,6 +132,7 @@ def evaluation_function(response: Any, answer: Any, params: Params) -> Result:
109132
diag="torch" | "ultralytics" | "model_exists" | "load_model" | "infer_once"
110133
"""
111134
items: List[Tuple[str, str]] = []
135+
t_handler0 = time.perf_counter()
112136

113137
try:
114138
fast_return: bool = bool(_pget(params, "fast_return", True))
@@ -117,7 +141,7 @@ def evaluation_function(response: Any, answer: Any, params: Params) -> Result:
117141
debug: bool = bool(_pget(params, "debug", True))
118142
skip_load_check: bool = bool(_pget(params, "skip_load_check", False))
119143

120-
# NEW: diag switch
144+
# diag switch
121145
diag: str = str(_pget(params, "diag", "none") or "none").strip().lower()
122146

123147
items.append(("SMOKE", "Hello / evaluation_function reached ✅"))
@@ -128,32 +152,46 @@ def evaluation_function(response: Any, answer: Any, params: Params) -> Result:
128152
items.append(("skip_load_check", str(skip_load_check)))
129153
items.append(("debug", str(debug)))
130154

131-
# ---- A) torch import (no image needed, but we keep flow consistent) ----
155+
# Always include a cold-start proxy timing snapshot early
156+
_add_common_timing(items, t_handler0)
157+
158+
# ----------------------------
159+
# A) torch lazy import timing
160+
# ----------------------------
132161
if diag == "torch":
133162
try:
134-
import torch # noqa
135-
import torch
136-
items.append(("A_torch", "import OK"))
163+
# Trigger actual import by touching a torch attribute
164+
_, dt = _timeit(lambda: torch.__version__)
165+
items.append(("A_torch", "lazy import OK"))
166+
items.append(("t_torch_import_s", f"{dt:.4f}"))
137167
items.append(("torch_version", str(torch.__version__)))
168+
# Extra checks (may trigger internal queries)
138169
items.append(("cuda_available", str(torch.cuda.is_available())))
170+
_add_common_timing(items, t_handler0)
139171
return _result(False, items)
140172
except Exception as e:
141173
items.append(("A_torch_FAIL", f"{type(e).__name__}: {e}"))
142174
items.append(("TRACEBACK", _escape_html(traceback.format_exc()).replace("\n", "<br>")))
175+
_add_common_timing(items, t_handler0)
143176
return _result(False, items)
144177

145-
# ---- B) ultralytics import ----
178+
# B) ultralytics lazy import timing
146179
if diag == "ultralytics":
147180
try:
148-
from ultralytics import YOLO # noqa: F401
149-
items.append(("B_ultralytics", "import OK"))
181+
# Trigger ultralytics import by accessing YOLO symbol
182+
YOLO, dt = _timeit(lambda: ultralytics.YOLO)
183+
items.append(("B_ultralytics", "lazy import OK"))
184+
items.append(("t_ultralytics_import_s", f"{dt:.4f}"))
185+
items.append(("YOLO_symbol", str(YOLO)))
186+
_add_common_timing(items, t_handler0)
150187
return _result(False, items)
151188
except Exception as e:
152189
items.append(("B_ultralytics_FAIL", f"{type(e).__name__}: {e}"))
153190
items.append(("TRACEBACK", _escape_html(traceback.format_exc()).replace("\n", "<br>")))
191+
_add_common_timing(items, t_handler0)
154192
return _result(False, items)
155193

156-
# ---- C) model existence ----
194+
# C) model existence
157195
if diag == "model_exists":
158196
paths = _candidate_model_paths()
159197
any_found = False
@@ -168,13 +206,15 @@ def evaluation_function(response: Any, answer: Any, params: Params) -> Result:
168206
if not any_found:
169207
items.append(("C_FAIL", "No model files found in candidate paths"))
170208
items.append(("C_candidates", " | ".join(paths)))
209+
_add_common_timing(items, t_handler0)
171210
return _result(False, items)
172211

173-
# ---- D/E require an image -> we continue below to load image first ----
212+
# D/E require an image -> continue to load image first
174213

175214
# 1) Validate input (unit test requirement)
176215
if not isinstance(response, list) or len(response) == 0:
177216
items.append(("BAD_INPUT", "No images uploaded."))
217+
_add_common_timing(items, t_handler0)
178218
return _result(False, items)
179219

180220
# 2) Extract first URL
@@ -191,15 +231,18 @@ def evaluation_function(response: Any, answer: Any, params: Params) -> Result:
191231

192232
if not url:
193233
items.append(("LOAD_FAIL", "LOAD_FAIL: first image has no url field"))
234+
_add_common_timing(items, t_handler0)
194235
return _result(False, items)
195236

196237
# 3) Load-check (keep your existing CI-safe behaviour)
197238
# D/E need an image anyway, so we must load here unless explicitly skipped.
198239
if (not skip_load_check) or try_fetch or diag in ("load_model", "infer_once"):
199-
img, err = _load_bgr_image_from_url(str(url))
240+
(img, err), dt_img = _timeit(lambda: _load_bgr_image_from_url(str(url)))
241+
items.append(("t_image_load_s", f"{dt_img:.4f}"))
200242
if img is None:
201243
items.append(("LOAD_FAIL", f"LOAD_FAIL: Failed to load image. ({err})"))
202244
items.append(("url", str(url)))
245+
_add_common_timing(items, t_handler0)
203246
return _result(False, items)
204247

205248
h, w = img.shape[:2]
@@ -208,59 +251,86 @@ def evaluation_function(response: Any, answer: Any, params: Params) -> Result:
208251
else:
209252
img = None # type: ignore
210253

211-
# ---- D) load model only (no inference) ----
254+
# D) load model only (no inference)
212255
if diag == "load_model":
213256
try:
214-
from ultralytics import YOLO
257+
# Trigger ultralytics import lazily
258+
YOLO, dt_ul = _timeit(lambda: ultralytics.YOLO)
259+
items.append(("t_ultralytics_import_s", f"{dt_ul:.4f}"))
260+
215261
model_path = next((p for p in _candidate_model_paths() if os.path.exists(p)), None)
216262
if not model_path:
217263
items.append(("D_FAIL", "No model file found to load"))
218264
items.append(("D_candidates", " | ".join(_candidate_model_paths())))
265+
_add_common_timing(items, t_handler0)
219266
return _result(False, items)
267+
220268
items.append(("D_model_path", model_path))
221-
_ = YOLO(model_path)
269+
270+
# Time model init/load
271+
_, dt_load = _timeit(lambda: YOLO(model_path))
272+
items.append(("t_model_load_s", f"{dt_load:.4f}"))
222273
items.append(("D_load", "model loaded ✅"))
274+
275+
_add_common_timing(items, t_handler0)
223276
return _result(False, items)
224277
except Exception as e:
225278
items.append(("D_FAIL", f"{type(e).__name__}: {e}"))
226279
items.append(("TRACEBACK", _escape_html(traceback.format_exc()).replace("\n", "<br>")))
280+
_add_common_timing(items, t_handler0)
227281
return _result(False, items)
228282

229-
# ---- E) infer once (minimal) ----
283+
# E) infer once (minimal)
230284
if diag == "infer_once":
231285
try:
232-
from ultralytics import YOLO
286+
# Trigger ultralytics import lazily
287+
YOLO, dt_ul = _timeit(lambda: ultralytics.YOLO)
288+
items.append(("t_ultralytics_import_s", f"{dt_ul:.4f}"))
289+
233290
model_path = next((p for p in _candidate_model_paths() if os.path.exists(p)), None)
234291
if not model_path:
235292
items.append(("E_FAIL", "No model file found for inference"))
236293
items.append(("E_candidates", " | ".join(_candidate_model_paths())))
294+
_add_common_timing(items, t_handler0)
237295
return _result(False, items)
238296

239297
if img is None:
240298
items.append(("E_FAIL", "Image not loaded (img is None)"))
299+
_add_common_timing(items, t_handler0)
241300
return _result(False, items)
242301

243302
items.append(("E_model_path", model_path))
244-
model = YOLO(model_path)
245-
_ = model.predict(source=img, imgsz=640, conf=0.25, verbose=False)
303+
304+
# Time YOLO() construction separately from predict()
305+
model, dt_load = _timeit(lambda: YOLO(model_path))
306+
items.append(("t_model_load_s", f"{dt_load:.4f}"))
307+
308+
_, dt_pred = _timeit(lambda: model.predict(source=img, imgsz=640, conf=0.25, verbose=False))
309+
items.append(("t_predict_s", f"{dt_pred:.4f}"))
310+
246311
items.append(("E_infer", "predict done ✅"))
312+
_add_common_timing(items, t_handler0)
247313
return _result(False, items)
248314
except Exception as e:
249315
items.append(("E_FAIL", f"{type(e).__name__}: {e}"))
250316
items.append(("TRACEBACK", _escape_html(traceback.format_exc()).replace("\n", "<br>")))
317+
_add_common_timing(items, t_handler0)
251318
return _result(False, items)
252319

253320
# 4) Optional early exit (keep your original platform smoke behaviour)
254321
if fast_return and not try_fetch:
255322
items.append(("note", "fast_return=True (no YOLO). Load-check already done."))
323+
_add_common_timing(items, t_handler0)
256324
return _result(False, items)
257325

258326
# 5) Default end (still no YOLO in this build unless you add it)
259327
items.append(("note", "No YOLO executed in default path. Use diag=... to pinpoint failures."))
328+
_add_common_timing(items, t_handler0)
260329
return _result(False, items)
261330

262331
except Exception as e:
263332
tb = _escape_html(traceback.format_exc())
264333
items.append(("UNHANDLED", f"{type(e).__name__}: {e}"))
265334
items.append(("TRACEBACK", tb.replace("\n", "<br>")))
266-
return _result(False, items)
335+
_add_common_timing(items, t_handler0)
336+
return _result(False, items)

evaluation_function/lazy_load.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import importlib
2+
3+
4+
class LazyModule:
5+
"""
6+
This class is response for lazy loading of heavy imports
7+
"""
8+
def __init__(self, name):
9+
self._name = name
10+
self._module = None
11+
12+
def _load(self):
13+
if self._module is None:
14+
self._module = importlib.import_module(self._name)
15+
return self._module
16+
17+
def __getattr__(self, item):
18+
return getattr(self._load(), item)
19+

evaluation_function/local_run.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99

1010
# ----local test image ----
11-
IMAGE_PATH = r"C:\Users\sheng\Desktop\Test2.jpg"
11+
IMAGE_PATH = r"C:\Users\sheng\Desktop\Test.jpg"
1212

1313
OUT_DIR = os.path.join(os.path.dirname(__file__), "_local_out")
1414
os.makedirs(OUT_DIR, exist_ok=True)

0 commit comments

Comments
 (0)