OC_P8 / scripts /profile_predict.py
KLEB38's picture
Upload folder using huggingface_hub
058898a verified
Raw
History Blame Contribute Delete
4.17 kB
"""Profile /predict with cProfile (étape 4 — bottleneck identification).
Runs N in-process /predict calls via FastAPI's TestClient and dumps a
``.prof`` file consumable by snakeviz. By default DATABASE_URL is unset
so the profile shows ONLY what remains on the critical path after the
BackgroundTask migration (handler = assembly + inference + response).
Usage:
uv run python scripts/profile_predict.py
uv run python scripts/profile_predict.py --n 200 --warmup 5
uv run snakeviz profiling/profile_predict.prof # interactive view
"""
from __future__ import annotations
import argparse
import cProfile
import logging
import os
import pstats
import random
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
logger = logging.getLogger("scripts.profile_predict")
logging.basicConfig(level=logging.INFO, format="%(message)s")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--n", type=int, default=100,
help="number of /predict calls to profile (default: 100)")
parser.add_argument("--warmup", type=int, default=3,
help="warm-up calls executed BEFORE profiling starts (default: 3)")
parser.add_argument("--output", type=Path,
default=REPO_ROOT / "profiling" / "profile_predict.prof",
help="output .prof path (default: ./profiling/profile_predict.prof)")
parser.add_argument("--seed", type=int, default=42,
help="payload sampling seed (default: 42)")
parser.add_argument("--top", type=int, default=25,
help="number of rows to print from the cumulative-time table")
args = parser.parse_args()
# Profile the handler ONLY. Unset DATABASE_URL so the BackgroundTask
# short-circuits — we want to see what the client actually waits on
# post-migration, not the deferred Supabase round-trip.
os.environ.pop("DATABASE_URL", None)
# Build payloads with the same logic used by seed_traffic — real rows
# from application_train.csv so the profile reflects realistic input
# distributions (mix of NaN, ints, floats, categorical strings).
from scripts.seed_traffic import APP_TRAIN_PATH, build_payloads
rng = random.Random(args.seed)
payloads = build_payloads(
APP_TRAIN_PATH, n_known=args.n + args.warmup, n_unknown=0, rng=rng
)
# Lazy import so DATABASE_URL change above is honoured by api.settings.
from fastapi.testclient import TestClient
from api.main import app
with TestClient(app) as client:
# Warm-up: pay one-time costs (lazy imports, JIT-like caches, pandas
# categorical materialisation) so they don't pollute the profile.
for payload in payloads[: args.warmup]:
resp = client.post("/predict", json=payload)
if resp.status_code != 200:
raise SystemExit(
f"Warm-up failed: status={resp.status_code} body={resp.text}"
)
logger.info("Warm-up: %d calls OK", args.warmup)
# Profiled window
profiler = cProfile.Profile()
profiler.enable()
for payload in payloads[args.warmup:]:
resp = client.post("/predict", json=payload)
if resp.status_code != 200:
logger.warning(
"Unexpected status %s during profiling: %s",
resp.status_code, resp.text[:200],
)
profiler.disable()
args.output.parent.mkdir(parents=True, exist_ok=True)
profiler.dump_stats(str(args.output))
logger.info("Profile saved to %s", args.output)
# Top-N by cumulative time — quick console view before snakeviz.
logger.info("\nTop %d functions by cumulative time:\n", args.top)
stats = pstats.Stats(profiler).sort_stats("cumulative")
stats.print_stats(args.top)
logger.info("\nVisualise interactively:\n uv run snakeviz %s\n", args.output)
return 0
if __name__ == "__main__":
raise SystemExit(main())