File size: 25,706 Bytes
0887b31 c699c4c 0887b31 c699c4c 0887b31 c699c4c 6ffd3f8 | 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 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 | # SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
# SPDX-License-Identifier: Apache-2.0
"""Host-only tests for the TT_FUSED path (torch only -- no ttnn import, no device).
Every exact reformulation the fused device graph relies on is checked here against the
legacy host math (``models.tt.postprocess``), plus the knob plumbing:
* NMS-T: the torch emulation of the exact device op sequence (slice -> untilize -> reshape ->
permute -> reshape -> [9,1] max-pool -> transpose -> [9,1] max-pool -> eq*mul -> transpose)
equals ``fold_scores(scores, r)`` (= fold + ``simple_nms``) bit for bit (``torch.equal``):
random softmax grids, a plateau/tie grid, border rows, batch 2, radii 1/4/8.
* Wide-page upload: the ``[1,1,N/32,32]`` host view is the same bytes as ``[1,1,N,1]``.
* rms_norm L2-norm: ``rms_norm(x, eps=0) * (1/16) == x / ||x||`` within 1 bf16 ULP of
``F.normalize`` (one final rounding; the legacy 4-op chain is reported alongside), and
``bf16(1/16) == 1/16`` exactly.
* ``postprocess_from_nms_map`` (the fused host post-processing) == ``postprocess_keypoints``.
* Knob: ``TT_FUSED`` unset/empty -> fused (default since 2026-09-13), ``0`` -> legacy (legacy
``CreateDevice`` kwargs); stages parsing.
* ``code/conftest.py`` (device fixtures for the device tests) imports without ttnn; device-id
resolution CLI > ``$TT_DEVICE_ID`` > ``$DEVICE_ID`` > 0.
Run (host python of the model's tt-metal tree; pytest is in that venv):
cd models/superpoint-p150/code && PYTHONPATH=. TT_METAL_HOME=<tree> \
<tree>/python_env/bin/python -m pytest -q models/tests/test_fused_host.py
or as a plain script: ``python models/tests/test_fused_host.py``.
"""
from __future__ import annotations
import sys
import pytest
import torch
import torch.nn.functional as F
from models.tt import fused_host as fh
from models.tt import postprocess as post
ENC_H, ENC_W = 60, 80 # 480x640 / 8
KEYPOINT_DIM = 65
def _softmax_scores(b: int, enc_h: int, enc_w: int, scale: float, seed: int) -> torch.Tensor:
"""Device-like softmaxed scores [1, 1, b*enc_h*enc_w, 65] in bf16."""
g = torch.Generator().manual_seed(seed)
logits = torch.randn(1, 1, b * enc_h * enc_w, KEYPOINT_DIM, generator=g) * scale
return torch.softmax(logits, dim=-1).to(torch.bfloat16)
def _to_nchw(s_sm: torch.Tensor, b: int, enc_h: int, enc_w: int) -> torch.Tensor:
"""What ``device_outputs_to_host`` does with ``s_sm``: NHWC -> NCHW fp32."""
return s_sm.reshape(b, enc_h, enc_w, KEYPOINT_DIM).permute(0, 3, 1, 2).contiguous().float()
# ------------------------------------------------------------------------------- NMS-T
@pytest.mark.parametrize("scale", [0.3, 3.0])
@pytest.mark.parametrize("radius", [4])
def test_nms_t_equals_fold_and_simple_nms_random(scale, radius):
s_sm = _softmax_scores(1, ENC_H, ENC_W, scale, seed=int(scale * 10))
ref = post.fold_scores(_to_nchw(s_sm, 1, ENC_H, ENC_W), radius)
out = fh.nms_t_reference(s_sm, 1, ENC_H, ENC_W, radius)
assert out.shape == (1, 480, 640) and out.dtype == torch.bfloat16
assert torch.equal(out.float(), ref)
assert (ref > 0).sum() > 100 # the test actually exercised suppression
def test_nms_t_pre_nms_fold_is_exact():
"""Radius-free check of the fold permutation alone: D[cy*8+i, cx*8+j] == s[cy, cx, i*8+j]."""
s_sm = _softmax_scores(1, ENC_H, ENC_W, 1.0, seed=7)
# radius 0 is not a device configuration (k=1 pool is pointless) but the emulation is
# well-defined: k=1, pad=0 -> identity pools, eq is all-true -> the folded map itself.
out = fh.nms_t_reference(s_sm, 1, ENC_H, ENC_W, 0)
ref = post.fold_scores(_to_nchw(s_sm, 1, ENC_H, ENC_W), None)
assert torch.equal(out.float(), ref)
def test_nms_t_ties_plateaus_and_borders():
"""Ties inside a window keep every tied pixel (== semantics), plateaus survive, and the
border rows/cols (windows partly outside the image, -inf padded) match the host."""
s_sm = _softmax_scores(1, ENC_H, ENC_W, 3.0, seed=3)
s = s_sm.clone()
s[:, :, :400] = 1.0 / 65 # first 5 cell rows: uniform -> every pixel ties with its window
s[..., 3] = s[..., 5] # channel ties in every cell
s[:, :, -80:, :64] = 0.0 # last cell row all zeros (border plateau of zeros)
s[:, :, 2000:2003, :] = s[:, :, 2000:2001, :] # three identical neighbouring cells
for radius in (1, 4, 8):
ref = post.fold_scores(_to_nchw(s, 1, ENC_H, ENC_W), radius)
out = fh.nms_t_reference(s, 1, ENC_H, ENC_W, radius)
assert torch.equal(out.float(), ref), f"radius {radius}"
# Sanity on the tie region: uniform rows whose 9x9 window stays inside the uniform block
# (rows 0..35; rows 36..39 see rows 40..43) are kept entirely -- the host tie semantics.
ref4 = post.fold_scores(_to_nchw(s, 1, ENC_H, ENC_W), 4)
assert torch.equal(ref4[0, :36], torch.full((36, 640), 1.0 / 65).to(torch.bfloat16).float())
def test_nms_t_batch_two():
s_sm = _softmax_scores(2, ENC_H, ENC_W, 2.0, seed=11)
ref = post.fold_scores(_to_nchw(s_sm, 2, ENC_H, ENC_W), 4)
out = fh.nms_t_reference(s_sm, 2, ENC_H, ENC_W, 4)
assert out.shape == (2, 480, 640)
assert torch.equal(out.float(), ref)
def test_nms_t_matches_a_realistic_score_map():
"""Peaky softmax outputs (a few dominant cells, most probability in the dustbin) --
the regime the real network produces -- plus the dustbin channel must be ignored."""
g = torch.Generator().manual_seed(5)
logits = torch.randn(1, 1, ENC_H * ENC_W, KEYPOINT_DIM, generator=g) * 0.5
logits[..., 64] += 4.0 # dustbin dominates most cells
peaks = torch.randint(0, ENC_H * ENC_W, (600,), generator=g)
chans = torch.randint(0, 64, (600,), generator=g)
logits[0, 0, peaks, chans] += 8.0
s_sm = torch.softmax(logits, dim=-1).to(torch.bfloat16)
nchw = _to_nchw(s_sm, 1, ENC_H, ENC_W)
ref = post.fold_scores(nchw, 4)
out = fh.nms_t_reference(s_sm, 1, ENC_H, ENC_W, 4)
assert torch.equal(out.float(), ref)
# and the dustbin really is dropped: nothing in the map exceeds the max non-dustbin score
assert out.float().max() <= nchw[:, :64].max()
assert int((ref > 0.005).sum()) >= 300 # suppression was exercised on a peaky map
def test_postprocess_from_nms_map_equals_postprocess_keypoints():
s_sm = _softmax_scores(1, ENC_H, ENC_W, 3.0, seed=21)
nchw = _to_nchw(s_sm, 1, ENC_H, ENC_W)
g = torch.Generator().manual_seed(22)
desc = F.normalize(torch.randn(1, 256, ENC_H, ENC_W, generator=g), dim=1)
kw = dict(keypoint_threshold=0.005, max_keypoints=1024, border_removal_distance=4, with_descriptors=True)
ref_kp, ref_sc, ref_desc = post.postprocess_keypoints(nchw, desc, nms_radius=4, **kw)[0]
nms_map = fh.nms_t_reference(s_sm, 1, ENC_H, ENC_W, 4).float() # what the device hands back
kp, sc, d = post.postprocess_from_nms_map(nms_map, desc, **kw)[0]
assert kp.shape[0] > 0
assert torch.equal(kp, ref_kp) and torch.equal(sc, ref_sc) and torch.equal(d, ref_desc)
# descriptors off / small top-k
kp2, sc2, d2 = post.postprocess_from_nms_map(nms_map, desc, **{**kw, "max_keypoints": 50, "with_descriptors": False})[0]
assert kp2.shape == (50, 2) and sc2.shape == (50,) and d2 is None
# ------------------------------------------------------------------------------- wide upload
def test_wide_input_view_is_the_same_bytes():
g = torch.Generator().manual_seed(1)
pixel = torch.rand(1, 3, 480, 640, generator=g)
nhwc = pixel[:, 0:1].permute(0, 2, 3, 1).reshape(1, 1, 480 * 640, 1).to(torch.bfloat16).contiguous()
wide = fh.wide_input_view(nhwc)
assert tuple(wide.shape) == fh.wide_input_shape(1, 480, 640) == (1, 1, 9600, 32)
assert wide.dtype == torch.bfloat16 and wide.is_contiguous()
assert wide.data_ptr() == nhwc.data_ptr() # a view, no copy
assert torch.equal(wide.view(torch.int16).flatten(), nhwc.view(torch.int16).flatten())
# element (row r, lane c) of the wide view is pixel r*32 + c of the flat NHWC input
assert torch.equal(wide[0, 0, 123], nhwc[0, 0, 123 * 32 : 124 * 32, 0])
with pytest.raises(ValueError):
fh.wide_input_view(torch.zeros(1, 1, 30, 1, dtype=torch.bfloat16))
# ------------------------------------------------------------------------------- rms_norm L2
def test_rms_gamma_is_exact_in_bf16():
assert fh.RMS_GAMMA == 1.0 / 16.0
assert torch.tensor(fh.RMS_GAMMA, dtype=torch.bfloat16).item() == fh.RMS_GAMMA
assert 16 * 16 == fh.DESCRIPTOR_DIM # gamma = 1/sqrt(D)
@pytest.mark.parametrize("scale", [0.05, 0.7, 20.0])
def test_l2norm_via_rms_within_one_bf16_ulp(scale):
g = torch.Generator().manual_seed(int(scale * 100))
x = (torch.randn(4800, 256, generator=g) * scale).to(torch.bfloat16)
ref = F.normalize(x.float(), p=2, dim=-1)
out = fh.l2norm_via_rms(x).float()
err = (out - ref).abs()
ulp = fh.bf16_ulp(ref)
# One final bf16 rounding of an fp32-exact value: <= 0.5 ULP (+ fp32 noise) -> allow 1 ULP.
# Reason 1 ULP and not torch.equal: the device rounds x*inv_rms*gamma once at the end; the
# reference F.normalize is fp32, so agreement is defined up to the output quantum.
assert (err <= ulp + 1e-12).all(), float((err / ulp).max())
assert float((err / ulp).max()) <= 0.5 + 1e-3
# Unit norm after the rounding (what smoke_test.py checks on the served descriptors).
assert (out.norm(dim=-1) - 1.0).abs().max() < 1e-2
# Report: the legacy 4-op chain rounds three times and is worse -- informational only.
legacy_err = ((fh.l2norm_legacy_chain(x).float() - ref).abs() / ulp).max()
assert legacy_err >= float((err / ulp).max()) - 1e-6
# ------------------------------------------------------------------------------- knob plumbing
def test_knob_default_is_fused_and_zero_is_legacy():
env = {}
assert fh.FUSED_DEFAULT is True
assert fh.fused_enabled(env) is True
assert fh.fused_enabled({"TT_FUSED": ""}) is True # empty == unset
assert fh.device_open_kwargs(0, 32768, fh.fused_enabled(env)) == {
"device_id": 0, "l1_small_size": 32768, "trace_region_size": fh.DEFAULT_TRACE_REGION,
}
for v in ("0", "off", "false", "no", " 0 "):
assert fh.fused_enabled({"TT_FUSED": v}) is False
# The legacy path keeps exactly the shipped CreateDevice kwargs.
assert fh.device_open_kwargs(0, 32768, fh.fused_enabled({"TT_FUSED": "0"})) == {"device_id": 0, "l1_small_size": 32768}
for v in ("1", "true", "YES", " on "):
assert fh.fused_enabled({"TT_FUSED": v}) is True
def test_stage_parsing():
assert fh.fused_stages({}) == frozenset(fh.ALL_STAGES) == {"wide", "nms", "rms", "rm", "l1", "nmsk", "kpc", "u8", "rsz"}
assert fh.fused_stages({"TT_FUSED_STAGES": ""}) == frozenset() # trace-only A/B
assert fh.fused_stages({"TT_FUSED_STAGES": "wide, NMS"}) == {"wide", "nms"}
assert fh.parse_stages(["rms", "rm"]) == {"rms", "rm"}
with pytest.raises(ValueError):
fh.fused_stages({"TT_FUSED_STAGES": "wide,bogus"})
def test_fused_device_open_kwargs():
assert fh.device_open_kwargs(0, 32768, True) == {
"device_id": 0, "l1_small_size": 32768, "trace_region_size": fh.DEFAULT_TRACE_REGION,
}
assert fh.device_open_kwargs(3, 32768, True, 6 * 1024 * 1024)["trace_region_size"] == 6 * 1024 * 1024
assert fh.trace_region_size({}) == fh.DEFAULT_TRACE_REGION >= 12 * 1024 * 1024
assert fh.trace_region_size({"SP_TRACE_REGION": "16777216"}) == 16777216
def test_fused_result_contract():
d = torch.zeros(1, 256, ENC_H, ENC_W)
r = fh.FusedResult(descriptors_nchw=d, nms_map=torch.zeros(1, 480, 640), nms_radius=4)
assert r.device_nms and r.scores_nchw is None
r2 = fh.FusedResult(descriptors_nchw=d, scores_nchw=torch.zeros(1, 65, ENC_H, ENC_W), nms_radius=7)
assert not r2.device_nms and r2.nms_map is None
def test_superpoint_ttnn_module_reads_knob_once_at_build(monkeypatch):
"""The device wrapper takes the knob from the ctor (``fused=None`` -> env) and stores the
decision; it must not consult the environment per call. Checked on the pure-python
surface without constructing a device model (ttnn may be absent on the host)."""
monkeypatch.delenv("TT_FUSED", raising=False)
assert fh.fused_enabled() is True
monkeypatch.setenv("TT_FUSED", "0")
assert fh.fused_enabled() is False
monkeypatch.setenv("TT_FUSED", "1")
assert fh.fused_enabled() is True
monkeypatch.setenv("TT_FUSED_STAGES", "nms")
assert fh.fused_stages() == {"nms"}
def test_repo_conftest_provides_device_fixtures():
"""The device tests take ``device`` / ``device_params`` / ``--device-id`` from
``code/conftest.py`` (tt-metal's conftest cannot be loaded next to this repo: ``code/models``
shadows its namespace ``models`` package). Import it by path -- no ttnn, no device -- and
check the pure device-id resolution (CLI > $TT_DEVICE_ID > $DEVICE_ID > 0)."""
import importlib.util
import pathlib
path = pathlib.Path(__file__).resolve().parents[2] / "conftest.py"
spec = importlib.util.spec_from_file_location("superpoint_repo_conftest", path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
assert callable(mod.pytest_addoption)
assert hasattr(mod, "device") and hasattr(mod, "device_params")
assert mod.resolve_device_id(None, {}) == 0
assert mod.resolve_device_id(None, {"DEVICE_ID": "3"}) == 3
assert mod.resolve_device_id(None, {"TT_DEVICE_ID": "2", "DEVICE_ID": "3"}) == 2
assert mod.resolve_device_id(None, {"TT_DEVICE_ID": ""}) == 0
assert mod.resolve_device_id(1, {"TT_DEVICE_ID": "2"}) == 1
assert (pathlib.Path(__file__).resolve().parents[2] / "pytest.ini").is_file()
def test_r_plane_u8_preprocess_is_bit_exact():
"""Serving fast path: R-band-only bilinear resize (uint8) + the U8_TO_BF16 table gives exactly
the bf16 channel-0 values of the original RGB resize + fp32 /255 + bf16 cast, for the sample
JPEG, a random RGB frame (both resize directions) and a palette image."""
import pathlib
import numpy as np
from PIL import Image
pytest.importorskip("fastapi")
from models.server import app as A
jpg = pathlib.Path(__file__).resolve().parents[2] / "sample_data" / "house_in_field_1080p.jpg"
rng = np.random.default_rng(0)
ims = [Image.open(jpg)]
for hw in ((900, 1600), (300, 401), (480, 640)):
ims.append(Image.fromarray(rng.integers(0, 256, (*hw, 3), dtype=np.uint8), "RGB"))
ims.append(Image.fromarray(rng.integers(0, 256, (123, 77), dtype=np.uint8), "L").convert("P"))
for im in ims:
im.load()
im = im if im.mode == "RGB" else im.convert("RGB")
ref = A._preprocess(im)[0, 0].to(torch.bfloat16)
got = fh.U8_TO_BF16[torch.from_numpy(A._preprocess_r8(im)).long()]
assert got.shape == ref.shape and torch.equal(got, ref)
@pytest.mark.parametrize("wh", [(1600, 900), (640, 480), (1920, 1080), (1280, 720), (641, 481), (639, 479), (320, 240),
(1001, 777), (3840, 2160), (2000, 480), (640, 1000), (17, 13), (4095, 3001), (1366, 768)])
def test_resize_r8_tables_are_pillow_exact(wh):
"""``rsz`` stage: the integer pipeline the device kernel runs (resize_r8.resize_ref, from the
host-built 22-bit tables) is bit-identical to Pillow's bilinear resize of an 8-bit band, for
down- / up-scales, odd sizes, one-axis resizes and identity, on random and smooth content."""
import numpy as np
from PIL import Image
from models.tt.resize_r8 import resize_ref
w, h = wh
rng = np.random.default_rng(w * 7 + h)
yy, xx = np.mgrid[0:h, 0:w]
for a in (rng.integers(0, 256, (h, w), dtype=np.uint8),
((np.sin(xx / 7.0) + np.cos(yy / 5.0)) * 60 + 128).clip(0, 255).astype(np.uint8)):
ref = np.asarray(Image.fromarray(a).resize((640, 480), Image.BILINEAR))
assert np.array_equal(resize_ref(a, 640, 480), ref)
def test_cell0_copy_schedule_and_tap_mapping_reproduce_the_conv():
"""Block-0 cell-tile conv (models/tt/conv_cell.py): the host copy schedule the reader kernel runs
(simulated on tile-memory byte order, 4 cores = 16 image rows incl. first / last core borders)
plus the compute kernel's tap -> tile mapping reproduce a 3x3 / pad-1 conv + bias + ReLU of the
[H, 640, 64] activation (fp32, bf16-rounded weights): every row shift, half-tile shift, zero row
and neighbour-core read is right."""
import numpy as np
from models.tt import conv_cell as C
def to_tiles(a):
R, Cc = a.shape
t = a.reshape(R // 32, 32, Cc // 32, 32).transpose(0, 2, 1, 3)
return t.reshape(R // 32, Cc // 32, 2, 16, 2, 16).transpose(0, 1, 2, 4, 3, 5).reshape(R // 32, Cc // 32, 1024)
def from_tile(t):
return t.reshape(2, 2, 16, 16).transpose(0, 2, 1, 3).reshape(32, 32)
rng = np.random.default_rng(0)
ncores = 4
H, W = 4 * ncores, 640
x = rng.standard_normal((H, W, 64)).astype(np.float32)
w = rng.standard_normal((64, 64, 3, 3)).astype(np.float32) * 0.1
b = rng.standard_normal(64).astype(np.float32)
wb = torch.from_numpy(w).to(torch.bfloat16).float()
bb = torch.from_numpy(b).to(torch.bfloat16).float()
ref = torch.relu(F.conv2d(torch.from_numpy(x).permute(2, 0, 1)[None], wb, bb, padding=1))[0].permute(1, 2, 0).numpy()
tiles = to_tiles(x.reshape(H * 80, 512)).reshape(ncores, C.TRS * C.QT, 1024)
wt = to_tiles(C.weight_matrix(torch.from_numpy(w), torch.from_numpy(b)).float().numpy())
W_t = [from_tile(wt[k // 2, k % 2]) for k in range(C.W_TILES + 2)]
out = np.zeros((H * 80, 512), np.float32)
calls = C.compute_calls(2)
assert C.compute_calls(4) and len(calls) == 4 * 3 * 4 * 2
for core in range(ncores):
groups = C.simulate(tiles, core, ncores)
for tr in range(C.TRS):
acc = np.zeros((8, 32, 64), np.float32) # [pixel, row, channel]
order = [[] for _ in range(8)]
for p0, ky, pp, h, w0, d, ct in calls:
src, idx = C.in0_index_pp(tr, pp, ky, h)
a = from_tile(tiles[core, idx] if src == "x" else groups[tr, idx])
for c in range(ct):
p, n = p0 + (d + c) // 2, (d + c) % 2
acc[p][:, n * 32:(n + 1) * 32] += a @ W_t[w0 + c]
order[p].append((ky, pp - p, h, n))
# every output tile gets each of its 18 (tap, half) products once, in af7b261's (ky, kx, h) order
for p in range(8):
for n in (0, 1):
assert [o[:3] for o in order[p] if o[3] == n] == [(ky, kx, h) for ky in (-1, 0, 1) for kx in (-1, 0, 1) for h in (0, 1)]
for p in range(8):
for n in (0, 1):
acc[p][:, n * 32:(n + 1) * 32] += W_t[C.W_TILES + n][0][None, :]
out[core * 320 + tr * 32: core * 320 + tr * 32 + 32, p * 64:(p + 1) * 64] = np.maximum(acc[p], 0)
assert np.abs(out.reshape(H, W, 64) - ref).max() < 1e-4
if __name__ == "__main__": # `python models/tests/test_fused_host.py` == `python -m pytest -q <file>`
sys.exit(pytest.main([__file__, "-q"]))
def test_b64decode_strict_matches_stdlib_validate():
"""server request decode: same bytes / same accept-reject set as base64.b64decode(validate=True)."""
import base64
import random
from models.server import app as A
rng = random.Random(0)
cases = ["", "QQ==", "QUI=", "QUJD", "QQ=", "QQ", "Q===", "QQ==QQ==", "AB=C", "AB CD", "QUJD\n", "QU-J",
"QUJD!", "ab+/", "ab_-", "====", "=QQQ", "QQ=="[:3], "é", "QUJDRA=="]
for n in (1, 2, 3, 10, 1000):
cases.append(base64.b64encode(bytes(rng.randrange(256) for _ in range(n))).decode())
for c in cases:
try:
want = base64.b64decode(c, validate=True)
except Exception:
want = None
try:
got = A._b64decode_strict(c)
except Exception:
got = None
assert got == want, (c, got, want)
def test_b64decode_strict_both_paths_match_stdlib_validate():
"""Both decoder branches (a2b_base64 strict_mode on Python >= 3.11, translate check before) give the same
accept / reject set and bytes as base64.b64decode(validate=True); the host venv is 3.10, the image 3.12."""
import base64
import binascii
import random
from models.server import app as A
rng = random.Random(1)
alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\n -_!"
cases = ["".join(rng.choice(alpha) for _ in range(rng.randrange(0, 13))) for _ in range(20000)]
cases += [base64.b64encode(bytes(rng.randrange(256) for _ in range(n))).decode() for n in range(0, 40)]
branches = [False] + ([True] if sys.version_info >= (3, 11) else [])
for strict in branches:
old = A._A2B_STRICT
A._A2B_STRICT = strict
try:
for c in cases:
try:
want = base64.b64decode(c, validate=True)
except binascii.Error:
want = None
try:
got = A._b64decode_strict(c)
except binascii.Error:
got = None
assert got == want, (strict, c, got, want)
finally:
A._A2B_STRICT = old
def test_fp16_cast_via_torch_equals_numpy_astype():
"""/predict descriptors: torch's fp32 -> fp16 cast gives numpy astype's bits (RNE, subnormals, inf, nan)."""
import numpy as np
g = torch.Generator().manual_seed(0)
x = torch.cat([torch.randn(200000, generator=g) * s for s in (1e-8, 1e-5, 1e-3, 0.1, 1.0, 100.0, 7e4)])
edge = torch.tensor([0.0, -0.0, 65504.0, 65519.99, 65520.0, -65520.0, 6.1e-5, 5.96e-8, 2.98e-8, 2.99e-8,
float("inf"), float("-inf"), float("nan"), 1.0 + 2 ** -11, 1.0 + 3 * 2 ** -11])
x = torch.cat([x, edge]).float()
a = x.numpy().astype(np.float16).view(np.uint16)
b = x.to(torch.float16).numpy().view(np.uint16)
nan = np.isnan(x.numpy())
assert np.array_equal(a[~nan], b[~nan]) and np.isnan(b.view(np.float16)[nan]).all()
def test_keypoint_list_build_equals_previous_form():
"""tolist() before round() gives the same Python floats as round(float(np.float64)) per element."""
import numpy as np
rng = np.random.default_rng(0)
kp = rng.uniform(0, 640, (5000, 2)).astype(np.float32).astype(np.float64) * np.array([2.5, 1.875])
sc = torch.from_numpy(rng.uniform(0, 1, 5000).astype(np.float32))
assert [[round(x, 3), round(y, 3)] for x, y in kp.tolist()] == [[round(float(x), 3), round(float(y), 3)] for x, y in kp]
assert [round(s, 6) for s in sc.tolist()] == [round(float(s), 6) for s in sc.tolist()]
def test_json_response_is_byte_identical_to_fastapi_dict_route():
"""/predict renders pydantic_core.to_json(resp): the same bytes FastAPI's ``-> dict`` route produced."""
pytest.importorskip("fastapi")
pytest.importorskip("httpx")
from fastapi import FastAPI
from fastapi.testclient import TestClient
from models.server import app as A
resp = {"num_keypoints": 3, "keypoints": [[610.0, 703.125], [0.1, 1e-05], [1e20, 123456789.123]],
"scores": [0.609375, 9.3e-05, 1e-06, 0.0], "original_size": {"height": 900, "width": 1600},
"scale": {"x": 2.5, "y": 1.875}, "params": {"keypoint_threshold": 1e-05, "nms_radius": 4},
"serving_path": {"traced": True, "device_nms": False}, "descriptors": {"data": "QUJD+/==", "shape": [3, 256]},
"s": "é\"\\\n<>&"}
ref = FastAPI()
@ref.post("/r")
def r() -> dict:
return resp
new = FastAPI()
@new.post("/r")
def n():
return A._json_response(resp)
a, b = TestClient(ref).post("/r"), TestClient(new).post("/r")
assert a.content == b.content and a.headers["content-type"] == b.headers["content-type"]
def test_binary_routes_validation_without_device():
"""/predict_raw and /predict_plane: query validation (400, same mapping as /predict), plane size check,
503 before the lifespan made the model ready; /predict unchanged in the same states."""
pytest.importorskip("fastapi")
pytest.importorskip("httpx")
from fastapi.testclient import TestClient
from models.server import app as A
assert not A.STATE.get("ready")
c = TestClient(A.app) # no `with`: the lifespan (device open) does not run
oct_ = {"content-type": "application/octet-stream"}
r = c.post("/predict_plane", params={"height": 3, "width": 3}, content=b"xx", headers=oct_)
assert r.status_code == 400 and "expected height*width=9" in r.json()["detail"]
r = c.post("/predict_plane", params={"height": 3, "width": 3, "nms_radius": 99}, content=b"x" * 9, headers=oct_)
assert r.status_code == 400 and r.json()["detail"][0]["loc"] == ["query", "nms_radius"]
r = c.post("/predict_plane", params={"width": 3}, content=b"x" * 9, headers=oct_)
assert r.status_code == 400 and r.json()["detail"][0]["loc"] == ["query", "height"]
assert c.post("/predict_plane", params={"height": 3, "width": 3}, content=b"x" * 9, headers=oct_).status_code == 503
assert c.post("/predict_raw", content=b"xx", headers=oct_).status_code == 503
r = c.post("/predict_raw", params={"max_keypoints": -2}, content=b"xx", headers=oct_)
assert r.status_code == 400 and r.json()["detail"][0]["loc"] == ["query", "max_keypoints"]
assert c.post("/predict", json={"image": "QUJD"}).status_code == 503
r = c.post("/predict", json={"image": "QUJD", "nms_radius": 99})
assert r.status_code == 400 and r.json()["detail"][0]["loc"] == ["body", "nms_radius"]
|