_glm53_recoverssm_test.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. #!/usr/bin/env python3
  2. """KDA RecoverSSM (experimental/fixes/recoverssm.py) against the stock spec path.
  3. Drives both through the same simulated speculative decode at GLM-5.3-Flash's
  4. per-rank KDA shapes (TP=4: 16 heads x 128, conv width 4), several layers per
  5. KV group, with batch-level k varying step to step (2, 3, 4, 5, 7) and each
  6. row accepting 0..k drafts.
  7. stock: causal_conv1d_update(num_accepted_tokens = last step's count)
  8. + fused_recurrent_kda with 1 + k state slots per request
  9. recoverssm: the same conv call with num_accepted_tokens = 1
  10. + kda_recoverssm_verify from one checkpoint block
  11. + Glm5NextRecoverSSMCommitContext.commit(num_sampled)
  12. Every step, per layer, bit for bit:
  13. - conv output and recurrent output for every verified token
  14. - the checkpoint is unchanged by the verify
  15. - after the commit: the recurrent state equals the stock slot n - 1, and the
  16. conv window at column 0 equals the stock window at column n - 1
  17. - align mode (prefix caching): the state is committed to the block holding
  18. the last committed token, and a step that crosses a block boundary also
  19. leaves the stock state after the boundary token in the boundary block
  20. Runs with and without request_indices (spec rows interleaved with other rows)
  21. and with a trailing cudagraph padding row.
  22. Pass: everything bit-identical. The summary counts checks and failures per
  23. check class, with the first failing step (later failures in a class can be
  24. fallout: the two paths then carry different states). RECOVERSSM_TEST_ATOL=<x>
  25. accepts differences up to x and reports them (diagnosis only).
  26. --drift N runs one chained config for N steps and prints how far the
  27. committed state and the outputs move from the stock path's.
  28. No weights needed. The module is found as vllm.models.glm5next.common.recoverssm
  29. when recoverssm.yaml mounts it, else RECOVERSSM_PY, else next to this file,
  30. else ../../experimental/fixes/recoverssm.py. On one GB10:
  31. mkdir -p /tmp/rs && cp dev/patch-tests/_glm53_recoverssm_test.py \\
  32. experimental/fixes/recoverssm.py /tmp/rs/
  33. sudo docker run --rm --gpus all --ipc=host -v /tmp/rs:/rs -w / \\
  34. --entrypoint python3 <glm53 image> /rs/_glm53_recoverssm_test.py [--bench]
  35. or `docker cp` both files into a glm53 container and `docker exec` python3 on
  36. the test. It allocates up to ~0.7 GB of GPU memory: run it on a box that is
  37. not serving, since a serving GB10 has 1.5-3 GiB free.
  38. --bench adds a timing of the stock spec kernel against verify + commit at
  39. 34 layers.
  40. """
  41. import importlib.util
  42. import os
  43. import sys
  44. from pathlib import Path
  45. from types import SimpleNamespace
  46. import torch
  47. from vllm.model_executor.layers.mamba.mamba_utils import is_conv_state_dim_first
  48. from vllm.model_executor.layers.mamba.ops.causal_conv1d import causal_conv1d_update
  49. from vllm.models.glm5next.nvidia.ops.third_party.kda import fused_recurrent_kda
  50. def _load_module():
  51. name = "vllm.models.glm5next.common.recoverssm"
  52. try:
  53. return importlib.import_module(name)
  54. except ImportError:
  55. pass
  56. here = Path(__file__).resolve().parent
  57. candidates = [
  58. os.environ.get("RECOVERSSM_PY"),
  59. here / "recoverssm.py",
  60. here.parent.parent / "experimental" / "fixes" / "recoverssm.py",
  61. ]
  62. for path in candidates:
  63. if path and Path(path).is_file():
  64. spec = importlib.util.spec_from_file_location(name, path)
  65. module = importlib.util.module_from_spec(spec)
  66. sys.modules[name] = module
  67. spec.loader.exec_module(module)
  68. print(f"recoverssm module: {path}")
  69. return module
  70. raise SystemExit("recoverssm.py not found; set RECOVERSSM_PY")
  71. rs = _load_module()
  72. dev = torch.device("cuda")
  73. H, D = 16, 128 # per rank at TP=4 (64 heads)
  74. PROJ = H * D
  75. CONV_DIM = 3 * PROJ
  76. WIDTH = 4
  77. LOWER_BOUND = -5.0
  78. DTYPE = torch.bfloat16
  79. LEVELS = (2, 3, 4, 5, 7)
  80. ATOL = float(os.environ.get("RECOVERSSM_TEST_ATOL", "0"))
  81. DS = is_conv_state_dim_first()
  82. CLASSES = (
  83. "conv output",
  84. "recurrent output",
  85. "checkpoint untouched",
  86. "committed state",
  87. "committed conv",
  88. "boundary state",
  89. "boundary conv",
  90. )
  91. class Checker:
  92. def __init__(self) -> None:
  93. self.checks = dict.fromkeys(CLASSES, 0)
  94. self.failures = dict.fromkeys(CLASSES, 0)
  95. self.inexact = dict.fromkeys(CLASSES, 0)
  96. self.worst = dict.fromkeys(CLASSES, 0.0)
  97. self.first: dict[str, str] = {}
  98. self.printed = 0
  99. def eq(self, cls: str, where: str, got: torch.Tensor, want: torch.Tensor) -> None:
  100. self.checks[cls] += 1
  101. if torch.equal(got, want):
  102. return
  103. diff = (got.float() - want.float()).abs()
  104. err = float(diff.nan_to_num(float("inf")).max())
  105. self.worst[cls] = max(self.worst[cls], err)
  106. if err <= ATOL:
  107. self.inexact[cls] += 1
  108. return
  109. self.failures[cls] += 1
  110. self.first.setdefault(cls, where)
  111. if self.printed < 10:
  112. self.printed += 1
  113. scale = float(want.float().abs().max())
  114. print(f" MISMATCH {cls} at {where}: max abs {err:.3g} (scale "
  115. f"{scale:.3g}), {int((diff > 0).sum())} of {diff.numel()} differ")
  116. def report(self) -> None:
  117. print(f"{'class':<22}{'checks':>8}{'fail':>8}{'inexact':>9}{'worst':>11} first failure")
  118. for cls in CLASSES:
  119. print(f"{cls:<22}{self.checks[cls]:>8}{self.failures[cls]:>8}"
  120. f"{self.inexact[cls]:>9}{self.worst[cls]:>11.3g} {self.first.get(cls, '-')}")
  121. def conv_pool(lines: int, state_len: int) -> tuple[torch.Tensor, torch.Tensor]:
  122. """(raw kv_cache[0] tensor, its (lines, dim, state_len) view for the kernels)."""
  123. if DS:
  124. raw = torch.zeros(lines, CONV_DIM, state_len, device=dev, dtype=DTYPE)
  125. return raw, raw
  126. raw = torch.zeros(lines, state_len, CONV_DIM, device=dev, dtype=DTYPE)
  127. return raw, raw.transpose(-1, -2)
  128. def make_layer(g: torch.Generator, lines_stock: int, lines_rs: int, k: int, rows: int):
  129. state_len = WIDTH - 1 + k
  130. layer = SimpleNamespace()
  131. layer.conv_w = (torch.randn(CONV_DIM, WIDTH, device=dev, generator=g) * 0.3).float()
  132. layer.A_log = torch.log(
  133. torch.rand(1, 1, H, 1, device=dev, generator=g) * 8 + 1
  134. ).float()
  135. layer.dt_bias = (torch.randn(PROJ, device=dev, generator=g) * 0.5).float()
  136. layer.kda_lower_bound = LOWER_BOUND
  137. layer.conv_s_raw, layer.conv_s = conv_pool(lines_stock, state_len)
  138. layer.rec_s = torch.zeros(lines_stock, H, D, D, device=dev, dtype=torch.float32)
  139. conv_r_raw, layer.conv_r = conv_pool(lines_rs, state_len)
  140. layer.rec_r = torch.zeros(lines_rs, H, D, D, device=dev, dtype=torch.float32)
  141. layer.kv_cache = (conv_r_raw, layer.rec_r)
  142. layer.recoverssm_records = rs.allocate_records(rows, H, D, k + 1, DTYPE, dev)
  143. return layer
  144. def stock_step(layer, x, g1, beta, qsl, cols, na_prev, k):
  145. y = causal_conv1d_update(
  146. x.clone(), layer.conv_s, layer.conv_w, None, activation="silu",
  147. conv_state_indices=cols[:, 0], num_accepted_tokens=na_prev,
  148. query_start_loc=qsl, max_query_len=k + 1,
  149. )
  150. q, kk, v = y.split(PROJ, dim=-1)
  151. o, _ = fused_recurrent_kda(
  152. q=q.reshape(1, -1, H, D), k=kk.reshape(1, -1, H, D), v=v.reshape(1, -1, H, D),
  153. g=g1, beta=beta, initial_state=layer.rec_s, use_qk_l2norm_in_kernel=True,
  154. cu_seqlens=qsl, ssm_state_indices=cols, num_accepted_tokens=na_prev,
  155. sigmoid_beta=True, a_log=layer.A_log, g_bias=layer.dt_bias,
  156. compute_gate=True, lower_bound=LOWER_BOUND,
  157. )
  158. return y, o
  159. def rs_step(layer, x, g1, beta, qsl, idx, ones, k):
  160. y = causal_conv1d_update(
  161. x.clone(), layer.conv_r, layer.conv_w, None, activation="silu",
  162. conv_state_indices=idx, num_accepted_tokens=ones,
  163. query_start_loc=qsl, max_query_len=k + 1,
  164. )
  165. q, kk, v = y.split(PROJ, dim=-1)
  166. o = rs.kda_recoverssm_verify(
  167. q=q.reshape(1, -1, H, D), k=kk.reshape(1, -1, H, D), v=v.reshape(1, -1, H, D),
  168. g=g1, beta=beta, a_log=layer.A_log, g_bias=layer.dt_bias,
  169. lower_bound=LOWER_BOUND, checkpoint_state=layer.rec_r,
  170. records=layer.recoverssm_records, query_start_loc=qsl, state_indices=idx,
  171. )
  172. return y, o
  173. def run(k: int, align: bool, seed: int, chk: Checker, steps: int = 40,
  174. num_reqs: int = 6, num_layers: int = 3, block_size: int = 16,
  175. drift: list | None = None) -> None:
  176. g = torch.Generator(device=dev).manual_seed(seed)
  177. cpu = torch.Generator().manual_seed(seed)
  178. B = num_reqs
  179. rows = B + 1 # one trailing padding row, as a FULL-graph batch has
  180. width = 2 + (steps * (k + 1)) // block_size + 2
  181. lines_stock = 1 + B * (k + 1)
  182. lines_rs = 1 + B * (width if align else 1)
  183. layers = [make_layer(g, lines_stock, lines_rs, k, rows) for _ in range(num_layers)]
  184. ctx = rs.Glm5NextRecoverSSMCommitContext.create(
  185. layers, spec_query_len=k + 1, max_num_reqs=rows
  186. )
  187. stock_cols = torch.zeros(rows, k + 1, dtype=torch.int32)
  188. for i in range(B):
  189. stock_cols[i] = torch.arange(1 + i * (k + 1), 1 + (i + 1) * (k + 1))
  190. stock_cols = stock_cols.to(dev)
  191. if align:
  192. block_table = torch.zeros(rows, width, dtype=torch.int32)
  193. for i in range(B):
  194. block_table[i] = torch.arange(1 + i * width, 1 + (i + 1) * width)
  195. block_table = block_table.to(dev)
  196. num_computed = torch.randint(1, 2 * block_size, (B,), generator=cpu)
  197. cur_col = ((num_computed - 1) // block_size).tolist()
  198. else:
  199. block_table = None
  200. num_computed = torch.zeros(B, dtype=torch.int64)
  201. cur_col = [0] * B
  202. def rs_block(i: int, col: int) -> int:
  203. return int(block_table[i, col]) if align else 1 + i
  204. # Same random checkpoint in both paths: stock slot 0, read with na = 1.
  205. for layer in layers:
  206. for i in range(B):
  207. state = torch.randn(H, D, D, device=dev, generator=g) * 0.1
  208. hist = torch.randn(CONV_DIM, WIDTH - 1, device=dev, generator=g).to(DTYPE)
  209. layer.rec_s[int(stock_cols[i, 0])] = state
  210. layer.conv_s[int(stock_cols[i, 0]), :, : WIDTH - 1] = hist
  211. b = rs_block(i, cur_col[i])
  212. layer.rec_r[b] = state
  213. layer.conv_r[b, :, : WIDTH - 1] = hist
  214. na_prev = torch.ones(rows, dtype=torch.int32, device=dev)
  215. ones = torch.ones(rows, dtype=torch.int32, device=dev)
  216. for step in range(steps):
  217. k_step = min(LEVELS[int(torch.randint(len(LEVELS), (1,), generator=cpu))], k)
  218. # Most rows verify k_step drafts. About one in five has no drafts.
  219. qlens = [
  220. 1 if torch.rand(1, generator=cpu) < 0.2 else k_step + 1 for _ in range(B)
  221. ] + [0]
  222. qsl_cpu = torch.zeros(rows + 1, dtype=torch.int32)
  223. qsl_cpu[1:] = torch.cumsum(torch.tensor(qlens), 0)
  224. qsl = qsl_cpu.to(dev)
  225. T = int(qsl_cpu[-1])
  226. x = torch.randn(T, CONV_DIM, device=dev, generator=g).to(DTYPE)
  227. g1 = torch.randn(1, T, H, D, device=dev, generator=g).to(DTYPE)
  228. # beta as a column slice of a wider projection, like kda.py's (its row
  229. # is 6416 wide: a multiple of 16, as here, which Triton specializes on).
  230. beta = torch.randn(1, T, H + 16, device=dev, generator=g).to(DTYPE)[..., :H]
  231. # Runner align preprocess: the running block moves to the column the
  232. # verify reads, (seq_len - 1) // block_size.
  233. if align:
  234. for i in range(B):
  235. dst = (int(num_computed[i]) + qlens[i] - 1) // block_size
  236. if dst != cur_col[i]:
  237. src_b, dst_b = rs_block(i, cur_col[i]), rs_block(i, dst)
  238. for layer in layers:
  239. layer.rec_r[dst_b] = layer.rec_r[src_b]
  240. layer.conv_r[dst_b] = layer.conv_r[src_b]
  241. cur_col[i] = dst
  242. idx = torch.tensor(
  243. [rs_block(i, cur_col[i]) for i in range(B)] + [0],
  244. dtype=torch.int32, device=dev,
  245. )
  246. for li, layer in enumerate(layers):
  247. before = layer.rec_r[idx[:B].long()].clone()
  248. y_s, o_s = stock_step(layer, x, g1, beta, qsl, stock_cols, na_prev, k)
  249. y_r, o_r = rs_step(layer, x, g1, beta, qsl, idx, ones, k)
  250. tag = f"k={k} align={align} seed={seed} step={step} layer={li}"
  251. chk.eq("conv output", tag, y_r[:T], y_s[:T])
  252. chk.eq("recurrent output", tag, o_r[:, :T], o_s[:, :T])
  253. chk.eq("checkpoint untouched", tag, layer.rec_r[idx[:B].long()], before)
  254. if drift is not None:
  255. out_err = float((o_r[:, :T].float() - o_s[:, :T].float()).abs().max())
  256. accepted = [int(torch.randint(0, q, (1,), generator=cpu)) for q in qlens[:B]]
  257. n_sampled = [a + 1 for a in accepted]
  258. # Odd steps: spec rows interleaved with other batch rows.
  259. interleave = step % 2 == 1
  260. if interleave:
  261. batch_rows = [2 * i + 1 for i in range(B)]
  262. nb = 2 * B + 1
  263. request_indices = torch.tensor(batch_rows, dtype=torch.int32, device=dev)
  264. else:
  265. batch_rows = list(range(B))
  266. nb = B
  267. request_indices = None
  268. num_sampled = torch.randint(0, 9, (nb,), generator=cpu, dtype=torch.int32)
  269. nc_batch = torch.randint(0, 99, (nb,), generator=cpu, dtype=torch.int32)
  270. bt_batch = torch.zeros(nb, width if align else 1, dtype=torch.int32)
  271. for i, r in enumerate(batch_rows):
  272. num_sampled[r] = n_sampled[i]
  273. nc_batch[r] = int(num_computed[i])
  274. if align:
  275. bt_batch[r] = block_table[i].cpu()
  276. align_kwargs = {}
  277. if align:
  278. align_kwargs = dict(
  279. block_table=bt_batch.to(dev),
  280. num_computed_tokens=nc_batch.to(dev),
  281. mamba_block_size=block_size,
  282. )
  283. ctx.commit(
  284. num_sampled.to(dev), idx[:B], qsl[: B + 1],
  285. request_indices=request_indices, **align_kwargs,
  286. )
  287. state_err = 0.0
  288. for i in range(B):
  289. n = n_sampled[i]
  290. nc = int(num_computed[i])
  291. # The block holding the last committed token.
  292. final_col = (nc + n - 1) // block_size if align else 0
  293. final_b = rs_block(i, final_col)
  294. for li, layer in enumerate(layers):
  295. tag = f"k={k} align={align} seed={seed} step={step} layer={li} req={i} n={n}"
  296. s_col = int(stock_cols[i, n - 1])
  297. c_col = int(stock_cols[i, 0])
  298. chk.eq("committed state", tag, layer.rec_r[final_b], layer.rec_s[s_col])
  299. chk.eq("committed conv", tag,
  300. layer.conv_r[final_b, :, : WIDTH - 1],
  301. layer.conv_s[c_col, :, n - 1 : n - 1 + WIDTH - 1])
  302. if drift is not None:
  303. want = layer.rec_s[s_col]
  304. state_err = max(state_err, float(
  305. (layer.rec_r[final_b] - want).abs().max() / want.abs().max()))
  306. next_boundary = (nc // block_size + 1) * block_size
  307. if align and nc + n >= next_boundary:
  308. blen = next_boundary - nc
  309. bb = rs_block(i, next_boundary // block_size - 1)
  310. chk.eq("boundary state", tag,
  311. layer.rec_r[bb], layer.rec_s[int(stock_cols[i, blen - 1])])
  312. chk.eq("boundary conv", tag,
  313. layer.conv_r[bb, :, : WIDTH - 1],
  314. layer.conv_s[c_col, :, blen - 1 : blen - 1 + WIDTH - 1])
  315. if align:
  316. num_computed[i] = nc + n
  317. cur_col[i] = final_col
  318. na_prev = torch.tensor(n_sampled + [1], dtype=torch.int32, device=dev)
  319. if drift is not None:
  320. drift.append((step, state_err, out_err))
  321. torch.cuda.synchronize()
  322. def drift_run(steps: int) -> None:
  323. """Chained, never resynced: how far RecoverSSM moves from the stock path."""
  324. samples: list = []
  325. run(7, False, 3, Checker(), steps=steps, num_layers=1, drift=samples)
  326. every = max(1, steps // 10)
  327. print(f"drift over {steps} steps (k=7, 6 requests; state error relative to "
  328. "the state's max, output error absolute):")
  329. worst_state = worst_out = 0.0
  330. for step, state_err, out_err in samples:
  331. worst_state = max(worst_state, state_err)
  332. worst_out = max(worst_out, out_err)
  333. if (step + 1) % every == 0:
  334. print(f" step {step + 1:>6}: state {state_err:.3g} (worst so far "
  335. f"{worst_state:.3g}), last-layer output {out_err:.3g} "
  336. f"(worst {worst_out:.3g})")
  337. def bench(num_reqs: int, k: int = 7, num_layers: int = 34, iters: int = 20) -> None:
  338. """Stock spec kernel vs verify + commit, all rows verifying k drafts.
  339. Layers share one pool here (timing only), so the numbers are per step for
  340. num_layers layers of one KV group.
  341. """
  342. g = torch.Generator(device=dev).manual_seed(0)
  343. q_len = k + 1
  344. rows = num_reqs
  345. layer = make_layer(g, 1 + rows * q_len, 1 + rows, k, rows)
  346. layer.rec_s.normal_(generator=g).mul_(0.1)
  347. layer.rec_r.normal_(generator=g).mul_(0.1)
  348. layers = [layer] * num_layers
  349. ctx = rs.Glm5NextRecoverSSMCommitContext.create(
  350. layers, spec_query_len=q_len, max_num_reqs=rows
  351. )
  352. T = rows * q_len
  353. qsl = torch.arange(0, T + 1, q_len, dtype=torch.int32, device=dev)
  354. cols = (1 + torch.arange(rows * q_len, dtype=torch.int32, device=dev)).view(rows, q_len)
  355. idx = 1 + torch.arange(rows, dtype=torch.int32, device=dev)
  356. na = torch.full((rows,), q_len, dtype=torch.int32, device=dev)
  357. y = torch.randn(T, CONV_DIM, device=dev, generator=g).to(DTYPE)
  358. q, kk, v = (t.reshape(1, -1, H, D) for t in y.split(PROJ, dim=-1))
  359. g1 = torch.randn(1, T, H, D, device=dev, generator=g).to(DTYPE)
  360. beta = torch.randn(1, T, H, device=dev, generator=g).to(DTYPE)
  361. out = torch.empty(1, T, H, D, device=dev, dtype=DTYPE)
  362. sampled = torch.full((rows,), q_len, dtype=torch.int32, device=dev)
  363. def stock():
  364. for _ in range(num_layers):
  365. fused_recurrent_kda(
  366. q=q, k=kk, v=v, g=g1, beta=beta, initial_state=layer.rec_s,
  367. use_qk_l2norm_in_kernel=True, cu_seqlens=qsl, ssm_state_indices=cols,
  368. num_accepted_tokens=na, out=out, sigmoid_beta=True, a_log=layer.A_log,
  369. g_bias=layer.dt_bias, compute_gate=True, lower_bound=LOWER_BOUND,
  370. )
  371. def recover():
  372. for _ in range(num_layers):
  373. rs.kda_recoverssm_verify(
  374. q=q, k=kk, v=v, g=g1, beta=beta, a_log=layer.A_log,
  375. g_bias=layer.dt_bias, lower_bound=LOWER_BOUND,
  376. checkpoint_state=layer.rec_r, records=layer.recoverssm_records,
  377. query_start_loc=qsl, state_indices=idx, out=out,
  378. )
  379. ctx.commit(sampled, idx, qsl)
  380. def verify_only():
  381. for _ in range(num_layers):
  382. rs.kda_recoverssm_verify(
  383. q=q, k=kk, v=v, g=g1, beta=beta, a_log=layer.A_log,
  384. g_bias=layer.dt_bias, lower_bound=LOWER_BOUND,
  385. checkpoint_state=layer.rec_r, records=layer.recoverssm_records,
  386. query_start_loc=qsl, state_indices=idx, out=out,
  387. )
  388. def timed(fn) -> float:
  389. fn()
  390. torch.cuda.synchronize()
  391. start, end = torch.cuda.Event(enable_timing=True), torch.cuda.Event(enable_timing=True)
  392. start.record()
  393. for _ in range(iters):
  394. fn()
  395. end.record()
  396. torch.cuda.synchronize()
  397. return start.elapsed_time(end) / iters
  398. t_stock, t_rec, t_ver = timed(stock), timed(recover), timed(verify_only)
  399. print(f"bench rows={rows} k={k} layers={num_layers}: stock {t_stock:.2f} ms, "
  400. f"verify {t_ver:.2f} ms + commit {t_rec - t_ver:.2f} ms = {t_rec:.2f} ms")
  401. chk = Checker()
  402. for k in (7, 3):
  403. for align in (False, True):
  404. for seed in (0, 1):
  405. run(k, align, seed, chk)
  406. print(f"k={k} align={align} seed={seed}: {sum(chk.checks.values())} "
  407. f"checks so far, {sum(chk.failures.values())} failures")
  408. chk.report()
  409. if "--bench" in sys.argv:
  410. for n in (16, 32, 64):
  411. bench(n)
  412. if "--drift" in sys.argv:
  413. drift_run(int(sys.argv[sys.argv.index("--drift") + 1]))
  414. checks, failures = sum(chk.checks.values()), sum(chk.failures.values())
  415. inexact = sum(chk.inexact.values())
  416. if failures:
  417. print(f"RECOVERSSM: FAIL ({failures} of {checks} checks)")
  418. sys.exit(1)
  419. if inexact:
  420. print(f"RECOVERSSM: PASS within atol {ATOL} ({inexact} of {checks} "
  421. "not bit-identical)")
  422. else:
  423. print(f"RECOVERSSM: PASS, bit-identical ({checks} checks)")