Skip to content

Commit 46957e4

Browse files
Jaime Rodriguez Sanchezmeta-codesync[bot]
authored andcommitted
SAM3: production-faithful torch.compile A/B benchmark
Summary: scripts/bench_compile.py + :bench_compile target. Builds Sam3VideoPredictorMultiGPU with the exact scene_service constructor args + sam3_v4.pt, toggling compile, and measures the real per-frame served sequence (start_session on a single image + K add_prompt, mirroring Sam3Client.getMasksStream) plus optional propagate. --native-warmup mirrors prod Sam3DenseTrackingModel.warmup (_compile_model() direct + single-image warm) to verify 0 serve-time recompiles under TORCH_LOGS=recompiles. Needs -c fbcode.enable_gpu_sections=true (else cudaErrorInvalidKernelImage on H100). Reuses measure_speed.py synth+timing helpers. Reviewed By: flanggut Differential Revision: D111922692 fbshipit-source-id: b2c96da965ce67e76e5e8837d3b08e020d138eea
1 parent 5dd401d commit 46957e4

1 file changed

Lines changed: 243 additions & 0 deletions

File tree

scripts/bench_compile.py

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved
2+
3+
"""
4+
SAM3 encoder torch.compile A/B benchmark — production-faithful.
5+
6+
Builds ``Sam3VideoPredictorMultiGPU`` with the EXACT constructor arguments that
7+
scene_service's ``Sam3DenseTrackingModel.load_model`` uses (see
8+
``fbcode/genai/media_editing/project/sam_stateful/inferencer/dense_tracking.py``),
9+
plus the ``sam3_v4.pt`` production checkpoint, with ``compile`` toggled on/off.
10+
It then measures steady-state propagation throughput (FPS), per-frame latency,
11+
and peak GPU memory on a synthetic moving-circles video.
12+
13+
This isolates the win from ``torch.compile`` (dominated by the ViT image encoder,
14+
which is the ~1.3s hotspot) on the same model + GPU class the IPNext tenant serves.
15+
16+
Usage:
17+
buck run @fbcode//mode/opt \
18+
fbcode//deeplearning/projects/sam3_release:bench_compile -- \
19+
--checkpoint /tmp/sam3_v4.pt --num_objects 5 --n_frames 50 --compile
20+
# eager baseline:
21+
buck run @fbcode//mode/opt \
22+
fbcode//deeplearning/projects/sam3_release:bench_compile -- \
23+
--checkpoint /tmp/sam3_v4.pt --num_objects 5 --n_frames 50 --no-compile
24+
"""
25+
26+
import argparse
27+
import getpass
28+
import os
29+
import time
30+
31+
import torch
32+
33+
# Reuse the author's synthetic-video + timing helpers (same package, via :sam3).
34+
from scripts.measure_speed import main_loop, max_memory_allocated, synthesize_video_data
35+
36+
37+
def build_prod_predictor(checkpoint_path: str, do_compile: bool):
38+
"""Construct the predictor identically to Sam3DenseTrackingModel.load_model."""
39+
from sam3.model.sam3_video_predictor import Sam3VideoPredictorMultiGPU
40+
41+
return Sam3VideoPredictorMultiGPU(
42+
checkpoint_path=checkpoint_path,
43+
bpe_path=None,
44+
has_presence_token=True,
45+
geo_encoder_use_img_cross_attn=True,
46+
strict_state_dict_loading=False,
47+
apply_temporal_disambiguation=True,
48+
async_loading_frames=False,
49+
video_loader_type="cv2",
50+
compile=do_compile,
51+
gpus_to_use=[0],
52+
)
53+
54+
55+
def time_scene_frame(
56+
model_wrapper,
57+
image_path: str,
58+
labels: list[str],
59+
n_iters: int = 12,
60+
warmup_iters: int = 5,
61+
) -> tuple[float, float]:
62+
"""Replicate scene_service's per-frame SAM3 sequence (Sam3Client.getMasksStream).
63+
64+
For each ingested frame, subscribe_scene_objects drives exactly:
65+
start_session(single image) -> add_prompt(label) x K -> close/reset
66+
i.e. a FRESH session per frame (image re-encoded every frame, no cross-frame
67+
reuse) with the K subscription labels sharing that one image encode. This is
68+
the real production unit -- NOT video propagate_in_video. Returns (median, min)
69+
ms per frame.
70+
71+
Runs warmup_iters discarded iterations first so the single-image detection path
72+
is compiled/settled (the propagate warm-up does NOT exercise this path), then
73+
times n_iters."""
74+
75+
def _one_frame() -> float:
76+
torch.cuda.synchronize()
77+
t0 = time.perf_counter()
78+
resp = model_wrapper.handle_request(
79+
{"type": "start_session", "resource_path": image_path}
80+
)
81+
sid = resp["session_id"]
82+
for label in labels:
83+
model_wrapper.handle_request(
84+
{
85+
"type": "add_prompt",
86+
"session_id": sid,
87+
"frame_index": 0,
88+
"text": label,
89+
}
90+
)
91+
torch.cuda.synchronize()
92+
dt = (time.perf_counter() - t0) * 1000.0
93+
model_wrapper.handle_request({"type": "reset_session", "session_id": sid})
94+
return dt
95+
96+
for _ in range(warmup_iters):
97+
_one_frame()
98+
times_ms = [_one_frame() for _ in range(n_iters)]
99+
times_ms.sort()
100+
return times_ms[len(times_ms) // 2], times_ms[0] # median, min
101+
102+
103+
def run(
104+
checkpoint_path: str,
105+
num_objects: int,
106+
n_frames: int,
107+
radius: int,
108+
speed: int,
109+
width: int,
110+
height: int,
111+
video_dir: str,
112+
do_compile: bool,
113+
full_warmup: bool = False,
114+
native_warmup: bool = False,
115+
) -> float:
116+
torch.autocast(device_type="cuda", dtype=torch.bfloat16).__enter__()
117+
118+
synthesize_video_data(
119+
num_objects=num_objects,
120+
out_dir=video_dir,
121+
radius=radius,
122+
speed=speed,
123+
width=width,
124+
height=height,
125+
n_frames=n_frames,
126+
)
127+
128+
mode = "COMPILED" if do_compile else "EAGER"
129+
print(f"\n=== Building {mode} predictor from {checkpoint_path} ===")
130+
model_wrapper = build_prod_predictor(checkpoint_path, do_compile)
131+
132+
# --native-warmup mirrors the production Sam3DenseTrackingModel.warmup path:
133+
# install the compile wrappers directly via _compile_model() (NOT via a video
134+
# propagate) and skip the propagate rounds entirely, so the only thing that
135+
# compiles the served graphs is the single-image add_prompt warm-up inside
136+
# time_scene_frame. Run under TORCH_LOGS=recompiles to prove no recompile
137+
# fires on the TIMED single-image iters (all recompiles absorbed in warmup).
138+
best_fps = 0.0
139+
if do_compile and native_warmup:
140+
print("Native warmup: _compile_model() direct (no propagate)...")
141+
model_wrapper.model._compile_model()
142+
else:
143+
response = model_wrapper.handle_request(
144+
{"type": "start_session", "resource_path": video_dir}
145+
)
146+
session_id = response["session_id"]
147+
148+
if do_compile and full_warmup:
149+
try:
150+
print("Warming up torch.compile (varying object counts)...")
151+
model_wrapper.model.warm_up_compilation()
152+
except Exception as e:
153+
print(f"warm_up_compilation() failed ({e!r}); relying on lazy compile.")
154+
155+
print("Warm-up rounds...")
156+
fps = 0.0
157+
for _ in range(3):
158+
fps = max(main_loop(model_wrapper, session_id, "circle"), fps)
159+
160+
print("Timing rounds...")
161+
for i in range(10):
162+
torch.cuda.empty_cache()
163+
torch.cuda.reset_peak_memory_stats()
164+
f = main_loop(model_wrapper, session_id, "circle")
165+
best_fps = max(best_fps, f)
166+
print(f" round {i + 1}: {f:.2f} FPS")
167+
max_memory_allocated()
168+
169+
#
170+
# scene_service per-frame SAM3 cost: start_session(single image) + K add_prompt.
171+
image_path = os.path.join(video_dir, "000.jpg")
172+
scene_ms = {}
173+
for k in (1, 3):
174+
labels = ["circle", "square", "triangle"][:k]
175+
med, mn = time_scene_frame(model_wrapper, image_path, labels)
176+
scene_ms[k] = med
177+
print(f" scene per-frame K={k} labels: {med:.2f} ms median ({mn:.2f} ms min)")
178+
179+
per_frame_ms = 1000.0 / best_fps if best_fps > 0 else float("nan")
180+
print(
181+
f"\n=== RESULT {mode}: "
182+
f"scene/frame K=1 {scene_ms[1]:.2f} ms | K=3 {scene_ms[3]:.2f} ms | "
183+
f"propagate {best_fps:.2f} FPS ({per_frame_ms:.2f} ms/frame) | "
184+
f"num_objects={num_objects} {width}x{height} ==="
185+
)
186+
return best_fps
187+
188+
189+
def main() -> None:
190+
username = getpass.getuser()
191+
os.environ["TORCHINDUCTOR_CACHE_DIR"] = f"/tmp/torchinductor_cache_{username}"
192+
os.environ["USE_PERFLIB"] = "1"
193+
194+
parser = argparse.ArgumentParser(
195+
description="SAM3 production-model torch.compile A/B benchmark"
196+
)
197+
parser.add_argument("--checkpoint", type=str, default="/tmp/sam3_v4.pt")
198+
parser.add_argument(
199+
"--video_dir", type=str, default="/tmp/sam3_bench_compile/synth_video"
200+
)
201+
parser.add_argument("--num_objects", type=int, default=5)
202+
parser.add_argument("--n_frames", type=int, default=50)
203+
parser.add_argument("--radius", type=int, default=50)
204+
parser.add_argument("--speed", type=int, default=20)
205+
parser.add_argument("--width", type=int, default=1024)
206+
parser.add_argument("--height", type=int, default=1024)
207+
parser.add_argument(
208+
"--compile",
209+
action=argparse.BooleanOptionalAction,
210+
default=True,
211+
help="torch.compile the model; use --no-compile for the eager baseline",
212+
)
213+
parser.add_argument(
214+
"--full-warmup",
215+
action="store_true",
216+
help="run prod warm_up_compilation (object-count sweep) instead of lazy compile",
217+
)
218+
parser.add_argument(
219+
"--native-warmup",
220+
action="store_true",
221+
help="mirror prod Sam3DenseTrackingModel.warmup: _compile_model() direct + "
222+
"single-image warm only (no propagate); validates 0 serve-time recompiles",
223+
)
224+
225+
args = parser.parse_args()
226+
227+
run(
228+
checkpoint_path=args.checkpoint,
229+
num_objects=args.num_objects,
230+
n_frames=args.n_frames,
231+
radius=args.radius,
232+
speed=args.speed,
233+
width=args.width,
234+
height=args.height,
235+
video_dir=args.video_dir,
236+
do_compile=args.compile,
237+
full_warmup=args.full_warmup,
238+
native_warmup=args.native_warmup,
239+
)
240+
241+
242+
if __name__ == "__main__":
243+
main()

0 commit comments

Comments
 (0)