File size: 8,929 Bytes
77e087f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | # SPDX-FileCopyrightText: © 2026 Tenstorrent Inc.
# SPDX-License-Identifier: Apache-2.0
"""First-call latency of the Python API, one call variant per FRESH process.
python code/scripts/first_call.py --variant right [--mano PATH] [--out .work/first_call] [-- from_pretrained kwargs as JSON]
It times ``HamerModel.from_pretrained(device_id=0)``, then 30 real calls of the variant. Every call has a NEW real input
(the 8 demo hands of ``media/``, the box shifted by ``k`` px from call 9 on), never the warm-up input. It prints and
writes ``<out>/<variant>.json``: boot seconds, the 1st / 2nd / 10th call, the median of calls 11..30, the
first-call penalty (1st - steady median), the median of the later calls on the same demo hand as call 1 (calls 9, 17, 25:
the crop size, and so the host time, depends on the hand), and a control: one more new call after a 0.5 s pause in the
warm process (the cost of the first call after ANY idle gap: cold CPU caches; warm-up cannot remove it). The raw outputs of every call go to ``<out>/<variant>.npz`` so two runs
(before / after a change) can be compared bit for bit.
Variants (the device graph has ONE shape: a batch-1 256x192 crop; every variant below is a host-side code path):
right model(path, bbox, is_right=True) single right hand, JPEG file path (the quickstart)
left model(path, bbox, is_right=False) single left hand (mirrored crop)
multi model(path, [box_a, box_b], is_right=[R, L]) two hands of one frame
array model(np.ndarray, bbox) decoded uint8 RGB frame
tensor model(torch.Tensor (3,H,W) uint8, bbox)
center model(path, bbox=None) centre-square crop
mano model(path, bbox, is_right=R/L) with MANO mesh, joints, 2D keypoints (needs --mano)
crops model.forward_crops(uint8 (2,3,256,256)) upstream-style batch, B=2
crops_float model.forward_crops(normalized float (2,3,256,256))
run_crop model.run_crop(uint8 (1,3,256,192))
big model(path, bbox) on 1920x1252 JPEG frames (the demo frames upscaled; written to <out>/big_frames)
"""
from __future__ import annotations
import argparse
import json
import os
import statistics
import sys
import time
from pathlib import Path
import numpy as np
REPO = Path(__file__).resolve().parents[2]
MEDIA = REPO / "media"
VARIANTS = ("right", "left", "multi", "array", "tensor", "center", "mano", "crops", "crops_float", "run_crop", "big")
BIG_SCALE = 3.75 # "big": the demo frames upscaled 512x334 -> 1920x1252 (larger than the 640x480 warm-up frame)
N_CALLS = 30
def hands():
boxes = json.loads((MEDIA / "bboxes.json").read_text())
return [(MEDIA / name, e["bbox"], bool(e.get("is_right", 1))) for name in sorted(boxes) for e in boxes[name]]
def inputs(variant: str, k: int, out: Path):
"""The k-th real input (k = 0..N_CALLS-1); distinct for every k."""
hs = hands()
if variant == "big":
from PIL import Image
path, box, right = hs[k % len(hs)]
d = k // len(hs)
big = out / "big_frames" / path.name
if not big.exists():
big.parent.mkdir(parents=True, exist_ok=True)
im = Image.open(path).convert("RGB")
im.resize((round(im.width * BIG_SCALE), round(im.height * BIG_SCALE)), Image.BICUBIC).save(big, quality=92)
return (big,), dict(bbox=[(v + d) * BIG_SCALE for v in box], is_right=right)
path, box, right = hs[k % len(hs)]
d = k // len(hs) # shift the box by d px on later rounds: a new crop every call
box = [box[0] + d, box[1] + d, box[2] + d, box[3] + d]
if variant == "right":
return (path,), dict(bbox=box, is_right=True)
if variant == "left":
return (path,), dict(bbox=box, is_right=False)
if variant == "mano":
return (path,), dict(bbox=box, is_right=right)
if variant == "multi":
p = sorted({h[0] for h in hs})[k % 4]
bs = [[b[0] + d, b[1] + d, b[2] + d, b[3] + d] for q, b, _ in hs if q == p]
return (p,), dict(bbox=bs, is_right=[r for q, _, r in hs if q == p])
if variant == "center":
return (hs[(k + d) % len(hs)][0],), dict(bbox=None) if k < 4 else dict(bbox=None, rescale_factor=2.0 - 0.01 * k)
from PIL import Image
a = np.asarray(Image.open(path).convert("RGB"))
if variant == "array":
return (a,), dict(bbox=box, is_right=right)
if variant == "tensor":
import torch
return (torch.from_numpy(a.copy()).permute(2, 0, 1),), dict(bbox=box, is_right=right)
from hamer.preprocess import crop_bbox
import torch
def crop256(i):
p, b, r = hs[(k * 2 + i) % len(hs)]
b = [b[0] + d, b[1] + d, b[2] + d, b[3] + d]
c, _ = crop_bbox(Image.open(p).convert("RGB"), b)
if not r:
c = c.transpose(Image.FLIP_LEFT_RIGHT)
return torch.from_numpy(np.asarray(c).copy()).permute(2, 0, 1)
if variant == "run_crop":
return (crop256(0)[None, :, :, 32:224].contiguous(),), {}
u = torch.stack([crop256(0), crop256(1)])
if variant == "crops":
return (u,), {}
if variant == "crops_float":
from hamer.preprocess import normalize_raw
return (normalize_raw(u),), {}
raise ValueError(variant)
def flat(out) -> np.ndarray:
"""All numbers of an output, for a bit-exact comparison."""
import torch
from hamer import HandPrediction
if isinstance(out, list):
return np.concatenate([flat(o) for o in out])
if isinstance(out, HandPrediction):
parts = [out.rotmats, out.betas, out.cam, out.cam_t_full, out.bbox_center, [out.bbox_size, out.focal_length]]
parts += [p for p in (out.vertices, out.joints, out.keypoints_2d) if p is not None]
return np.concatenate([np.asarray(p, np.float64).ravel() for p in parts])
if isinstance(out, dict):
return np.concatenate([flat(out[k]) for k in sorted(out)])
if isinstance(out, torch.Tensor):
return out.detach().double().numpy().ravel()
raise TypeError(type(out))
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--variant", required=True, choices=VARIANTS)
ap.add_argument("--mano", default=None)
ap.add_argument("--out", default=str(REPO / ".work" / "first_call"))
ap.add_argument("--kwargs", default="{}", help="extra from_pretrained kwargs as JSON (e.g. warmup_variants)")
a = ap.parse_args()
if a.variant == "mano" and not a.mano:
sys.exit("--variant mano needs --mano")
real = [inputs(a.variant, k, Path(a.out)) for k in range(N_CALLS + 1)] # host-side input preparation is not part of the timing
t0 = time.perf_counter()
import torch # noqa: F401 (timed as part of a user's start-up)
from hamer import HamerModel
t_imp = time.perf_counter() - t0
t0 = time.perf_counter()
model = HamerModel.from_pretrained(device_id=0, mano_path=a.mano, **json.loads(a.kwargs))
t_boot = time.perf_counter() - t0
fn = {"crops": model.forward_crops, "crops_float": model.forward_crops, "run_crop": model.run_crop}.get(a.variant, model)
ts, outs = [], []
for args, kw in real[:N_CALLS]:
t = time.perf_counter()
out = fn(*args, **kw)
ts.append((time.perf_counter() - t) * 1e3)
outs.append(flat(out))
time.sleep(0.5)
t = time.perf_counter()
fn(*real[N_CALLS][0], **real[N_CALLS][1])
idle_ms = (time.perf_counter() - t) * 1e3
model.close()
steady = statistics.median(ts[10:])
per = 4 if a.variant in ("multi", "crops", "crops_float") else 8 # inputs repeat (box shifted) with this period
same = statistics.median(ts[per::per][:3])
res = {"variant": a.variant, "import_s": round(t_imp, 2), "from_pretrained_s": round(t_boot, 2),
"call1_ms": round(ts[0], 2), "call2_ms": round(ts[1], 2), "call10_ms": round(ts[9], 2),
"steady_median_ms": round(steady, 2), "first_call_penalty_ms": round(ts[0] - steady, 2),
"call1_over_steady": round(ts[0] / steady, 3), "same_input_median_ms": round(same, 2),
"call1_over_same_input": round(ts[0] / same, 3), "after_idle_ms": round(idle_ms, 2),
"after_idle_over_same_input": round(idle_ms / statistics.median(ts[N_CALLS % per + per::per]), 3),
"warmup": model.info.get("warmup"),
"warmup_variants": model.info.get("warmup_variants"), "chip": os.environ.get("TT_CHIP"),
"host_load": [round(v, 1) for v in os.getloadavg()], "all_ms": [round(v, 3) for v in ts]}
out = Path(a.out)
out.mkdir(parents=True, exist_ok=True)
(out / f"{a.variant}.json").write_text(json.dumps(res, indent=1))
np.savez(out / f"{a.variant}.npz", *outs)
print("[first_call] " + json.dumps({k: v for k, v in res.items() if k not in ("all_ms", "warmup")}), flush=True)
if __name__ == "__main__":
main()
|