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()