Quazim0t0 commited on
Commit
54e984e
·
verified ·
1 Parent(s): 445aa36

verified: bound GEMM memory, certify the multiply LUT, test the shipped paths

Browse files

LUTBackend.gemm and NeuralBackend.gemm materialized the full (m,n,k) product block before reducing, making peak memory cubic in layer width -- 2.4 GB for one layer's forward at the SpikeWhale panel's maximum settings. Both now block the m axis; the contraction axis k is untouched, so results are bit-identical (512x768x768: 2426.9 MB -> 77.2 MB; neural 64x96x96: 519.2 MB -> 57.5 MB).

The two backends need different block accounting: a LUT product costs 8 bytes, a neural product ~880 (four nibble pairs through a 128-wide net).

build_luts() returned its tables unchecked while the docs described a self-certify gate. The table is the complete finite domain, so certifying it IS the exhaustive verification: LUTBackend now checks all 65536 entries against signed integer multiply at construction and raises on mismatch (0.5 ms).

Exhaustive verification covered the scalar entry points; production runs the batched paths and the LUTs built from them, which are different code. test_verified_units.py checks every shipped path against golden integer arithmetic over its full domain (24 checks, all passing).

gemm_int8 enforces the int32 accumulator bound (K <= 131071) rather than asserting it in a comment. instrument.require() fails on under-invocation and refuses to report while counting is disabled.

README.md CHANGED
@@ -363,6 +363,7 @@ docker/ Dockerfile, dashboard image, compose (demo cluster)
363
  scripts/setup.bat / setup.sh interactive setup helpers
364
  config/ nodes + cluster env examples
365
  examples/my_task_template.py starting point for your own model
 
366
  docs/ QUICKSTART, LIMITS, CUSTOM_TASK, TAILSCALE
367
  daisychain/spikewhale_task.py trains the real SpikeWhale on streamed HF datasets
368
  daisychain/spikewhale_panel.py slider control panel (localhost:8899)
@@ -370,6 +371,56 @@ web/ DaisyChain-Web: P2P browser training (WebRTC + WebG
370
  export_luts_web.py regenerates web/public LUTs from the trained units
371
  ```
372
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
  ## Install
374
  ```bash
375
  pip install torch numpy psutil
 
363
  scripts/setup.bat / setup.sh interactive setup helpers
364
  config/ nodes + cluster env examples
365
  examples/my_task_template.py starting point for your own model
366
+ test_verified_units.py full-domain checks of every shipped verified path (24)
367
  docs/ QUICKSTART, LIMITS, CUSTOM_TASK, TAILSCALE
368
  daisychain/spikewhale_task.py trains the real SpikeWhale on streamed HF datasets
369
  daisychain/spikewhale_panel.py slider control panel (localhost:8899)
 
371
  export_luts_web.py regenerates web/public LUTs from the trained units
372
  ```
373
 
374
+ ## Recent updates (August 2026) — verified compute path
375
+
376
+ Three changes to `daisychain/verified/`, each measured rather than asserted.
377
+ `python test_verified_units.py` (24 checks) reproduces all of it.
378
+
379
+ **Bounded GEMM memory.** `LUTBackend.gemm` and `NeuralBackend.gemm` materialized
380
+ the whole `(m, n, k)` product block before reducing it, so peak memory was cubic
381
+ in layer width. At the SpikeWhale panel's maximum (hidden 768, sequence 512) a
382
+ single layer's forward allocated **2.4 GB** — on the spare hardware this project
383
+ exists to use. Both now block the `m` axis. The contraction axis `k` is untouched,
384
+ so the sum and its order are unchanged and results stay bit-identical:
385
+
386
+ | GEMM | before | after |
387
+ | --- | --- | --- |
388
+ | 64x64x64 | 2.3 MB | 2.3 MB (unchanged path) |
389
+ | 256x256x256 | 135.8 MB | 68.9 MB |
390
+ | 512x768x768 (panel max) | 2426.9 MB | **77.2 MB** |
391
+ | neural backend, 64x96x96 | 519.2 MB | **57.5 MB** |
392
+
393
+ Caps are class attributes (`LUTBackend.max_block_bytes`,
394
+ `NeuralBackend.max_products`), tunable per instance. Below the cap the LUT path
395
+ takes the original single-block branch unchanged.
396
+
397
+ The two backends need *different* accounting, which is worth knowing before tuning
398
+ them: a LUT product costs 8 bytes, but a neural product is split into four nibble
399
+ pairs through a 128-wide net and costs ~880 bytes. Applying the LUT's byte budget
400
+ to the neural path computes a block 110x too large and never splits at all.
401
+
402
+ **The multiply LUT is certified, not trusted.** `build_luts()` returned its tables
403
+ unchecked while the docs described a self-certify gate. Because the table *is* the
404
+ complete finite domain, certifying it **is** the exhaustive verification:
405
+ `LUTBackend.__init__` now checks all 65536 entries against signed integer multiply
406
+ and raises on mismatch. Cost: **0.5 ms**.
407
+
408
+ **Full-domain tests for the paths that ship.** The units' exhaustive verification
409
+ covers the scalar entry points (`NeuralMul8.verify()` walks `mul()`); production
410
+ runs the batched ones — `mul_array`, `relu_array`, `requant_array` — and the LUTs
411
+ built from them, which are different code. `test_verified_units.py` checks every
412
+ shipped path against golden integer arithmetic over its complete domain. All pass
413
+ today; nothing would have caught a regression before.
414
+
415
+ **Two guards against silent no-ops.** `gemm_int8` enforces the int32 accumulator
416
+ bound (`MAX_INT32_K = 131071`) instead of asserting it in a comment, since layer
417
+ width is user-selectable and an overflow would produce wrong numbers rather than
418
+ raise. `instrument.require(**minimums)` fails when a verified unit was
419
+ under-invoked, and refuses to report at all while counting is disabled — a zero
420
+ from a probe that was switched off is not evidence that the units did not run.
421
+
422
+ ---
423
+
424
  ## Install
425
  ```bash
426
  pip install torch numpy psutil
daisychain/verified/backends.py CHANGED
@@ -42,16 +42,38 @@ class NeuralBackend:
42
  def available(self) -> bool:
43
  return hasattr(self.mul, "mul_array")
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  def gemm(self, A: np.ndarray, B: np.ndarray) -> np.ndarray:
46
  A = np.asarray(A).astype(np.int64)
47
  B = np.asarray(B).astype(np.int64)
48
  m, k = A.shape
49
  _, n = B.shape
50
- # all (A[i,t], B[t,j]) pairs -> one batched neural multiply -> sum over k
51
- Ai = np.broadcast_to(A[:, None, :], (m, n, k)) # (m,n,k)
52
- Bj = np.broadcast_to(B.T[None, :, :], (m, n, k)) # (m,n,k)
53
- prod = self.mul.mul_array(Ai.reshape(-1), Bj.reshape(-1)).reshape(m, n, k)
54
- return prod.sum(axis=2).astype(np.int64)
 
 
 
 
 
 
 
55
 
56
 
57
  def pick_backend(neural=None):
 
42
  def available(self) -> bool:
43
  return hasattr(self.mul, "mul_array")
44
 
45
+ #: Cap on PRODUCTS (m*n*k entries) handed to one batched multiply.
46
+ #:
47
+ #: Counted in products, not bytes, because bytes-per-product here is not the
48
+ #: 8 of an int64 table lookup: `reshape(-1)` materializes the broadcast pair
49
+ #: arrays, `mul_array` then splits each product into FOUR nibble pairs, and
50
+ #: each pair goes through a 128-wide net whose activations dominate everything
51
+ #: else. Measured on the numpy side alone: ~880 bytes per product (589,824
52
+ #: products -> 519 MB peak), and torch's own allocations sit on top of that.
53
+ #:
54
+ #: Applying LUTBackend's m*n*k*8 accounting here -- which the first version of
55
+ #: this patch did -- computes a block 110x too large and never splits at all.
56
+ #: The contraction axis k is untouched either way, so blocking cannot change
57
+ #: the sum or its order: the result stays bit-identical.
58
+ max_products = 1 << 16
59
+
60
  def gemm(self, A: np.ndarray, B: np.ndarray) -> np.ndarray:
61
  A = np.asarray(A).astype(np.int64)
62
  B = np.asarray(B).astype(np.int64)
63
  m, k = A.shape
64
  _, n = B.shape
65
+ Bt = B.T[None, :, :]
66
+ out = np.empty((m, n), dtype=np.int64)
67
+ rows = max(1, int(self.max_products // max(1, n * k)))
68
+ for i in range(0, m, rows):
69
+ r = A[i:i + rows]
70
+ rn = r.shape[0]
71
+ # all (A[i,t], B[t,j]) pairs in this block -> one batched neural multiply
72
+ Ai = np.broadcast_to(r[:, None, :], (rn, n, k))
73
+ Bj = np.broadcast_to(Bt, (rn, n, k))
74
+ prod = self.mul.mul_array(Ai.reshape(-1), Bj.reshape(-1)).reshape(rn, n, k)
75
+ out[i:i + rows] = prod.sum(axis=2)
76
+ return out.astype(np.int64)
77
 
78
 
79
  def pick_backend(neural=None):
daisychain/verified/instrument.py CHANGED
@@ -32,3 +32,42 @@ def bump(key: str, n: int = 1):
32
 
33
  def report() -> dict:
34
  return dict(COUNTS)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
  def report() -> dict:
34
  return dict(COUNTS)
35
+
36
+
37
+ def enabled() -> bool:
38
+ """Whether counting is on.
39
+
40
+ Callers need this to tell "the units did not run" apart from "nobody was
41
+ counting". A zero count from a disabled instrument is not evidence about the
42
+ units; reporting it as one is how a silent no-op passes for a result.
43
+ """
44
+ return _ENABLED
45
+
46
+
47
+ def require(**minimums: int) -> dict:
48
+ """Assert each counter reached a minimum, and return the counts.
49
+
50
+ The counters are the evidence that compute went THROUGH the verified units
51
+ rather than around them -- but nothing forces anyone to look. A run where the
52
+ units never fired otherwise produces the same silence as a run where they
53
+ fired and the result was unremarkable.
54
+
55
+ instrument.enable()
56
+ ... train ...
57
+ instrument.require(**{"NeuralMul4.forward_calls": 1})
58
+
59
+ Raises RuntimeError if counting is disabled (a zero here would be an artifact
60
+ of the probe, not a fact about the run) or if any counter fell short.
61
+ """
62
+ if not _ENABLED:
63
+ raise RuntimeError(
64
+ "instrument.require() called while counting is disabled -- enable() "
65
+ "first, or the zero counts say nothing about whether the units ran")
66
+ short = {k: (COUNTS.get(k, 0), n) for k, n in minimums.items()
67
+ if COUNTS.get(k, 0) < n}
68
+ if short:
69
+ raise AssertionError(
70
+ "verified units under-invoked: "
71
+ + ", ".join("%s=%d (want >=%d)" % (k, got, want)
72
+ for k, (got, want) in sorted(short.items())))
73
+ return dict(COUNTS)
daisychain/verified/kernel.py CHANGED
@@ -11,11 +11,24 @@ from __future__ import annotations
11
  import numpy as np
12
 
13
 
 
 
 
 
 
 
 
 
14
  def gemm_int8(A: np.ndarray, B: np.ndarray) -> np.ndarray:
15
  """Signed INT8 GEMM with exact int32 accumulation (tensor-core semantics)."""
 
 
 
 
 
16
  a = A.astype(np.int32)
17
  b = B.astype(np.int32)
18
- return a @ b # exact integer matmul, no overflow for our sizes
19
 
20
 
21
  def random_int8(shape, rng) -> np.ndarray:
 
11
  import numpy as np
12
 
13
 
14
+ #: Largest contraction length an int32 accumulator can take without overflow.
15
+ #: |a*b| <= 128*128 = 16384 for int8 operands, so K*16384 must stay inside int32.
16
+ #: The bound is real headroom (131071), but layer width is user-selectable from the
17
+ #: SpikeWhale panel, so it is checked rather than asserted in a comment -- an
18
+ #: overflow here would silently produce wrong numbers, not raise.
19
+ MAX_INT32_K = (2 ** 31 - 1) // (128 * 128)
20
+
21
+
22
  def gemm_int8(A: np.ndarray, B: np.ndarray) -> np.ndarray:
23
  """Signed INT8 GEMM with exact int32 accumulation (tensor-core semantics)."""
24
+ k = A.shape[-1]
25
+ if k > MAX_INT32_K:
26
+ raise ValueError(
27
+ "contraction length %d exceeds int32 accumulator headroom %d; "
28
+ "accumulate in int64 for this size" % (k, MAX_INT32_K))
29
  a = A.astype(np.int32)
30
  b = B.astype(np.int32)
31
+ return a @ b # exact integer matmul within the checked bound
32
 
33
 
34
  def random_int8(shape, rng) -> np.ndarray:
daisychain/verified/lut.py CHANGED
@@ -34,12 +34,43 @@ def build_relu8_lut(relu) -> np.ndarray:
34
  return relu.relu_array(np.arange(256)).astype(np.int64)
35
 
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  class LUTBackend:
38
  """GEMM via the materialized (verified) multiply table + integer accumulate."""
39
  name = "lut"
40
 
41
- def __init__(self, mul):
 
 
 
 
 
 
 
 
 
42
  self.mul_lut = build_mul8_lut(mul)
 
 
 
 
 
 
 
43
 
44
  def available(self):
45
  return True
@@ -47,9 +78,17 @@ class LUTBackend:
47
  def gemm(self, A: np.ndarray, B: np.ndarray) -> np.ndarray:
48
  au = (A.astype(np.int64) & 0xFF)
49
  bu = (B.astype(np.int64) & 0xFF)
50
- # products via table lookup, then sum over the contraction axis
51
- prod = self.mul_lut[au[:, None, :], bu.T[None, :, :]] # (m, n, k)
52
  from . import instrument
53
  instrument.bump("VerifiedMul(LUT).gemms", 1)
54
- instrument.bump("VerifiedMul(LUT).products", prod.size)
55
- return prod.sum(axis=2)
 
 
 
 
 
 
 
 
 
34
  return relu.relu_array(np.arange(256)).astype(np.int64)
35
 
36
 
37
+ def certify_mul8_lut(lut: np.ndarray) -> tuple[int, int]:
38
+ """Check the materialized multiply table against signed integer multiply.
39
+
40
+ The table IS the entire finite domain, so certifying it is the exhaustive
41
+ verification -- not a sample of it. Costs ~0.5 ms, which is why there is no
42
+ reason to take the guarantee on trust at runtime.
43
+ """
44
+ a = np.arange(256)
45
+ au, bu = np.repeat(a, 256), np.tile(a, 256)
46
+ sa = np.where(au >= 128, au - 256, au)
47
+ sb = np.where(bu >= 128, bu - 256, bu)
48
+ got = lut[au, bu].ravel()
49
+ return int((got == sa * sb).sum()), got.size
50
+
51
+
52
  class LUTBackend:
53
  """GEMM via the materialized (verified) multiply table + integer accumulate."""
54
  name = "lut"
55
 
56
+ #: Cap on the (rows, n, k) product block held at once, in bytes. The block is
57
+ #: an int64 temporary of m*n*k entries, so an unchunked GEMM allocates
58
+ #: m*n*k*8 -- cubic in layer width. At the panel's maximum settings
59
+ #: (hidden 768, seqlen 512) that is 2.4 GB for ONE layer's forward, on
60
+ #: machines this project targets precisely because they are small. Blocking
61
+ #: the m axis bounds it; the contraction axis k is untouched, so the sum and
62
+ #: its order are unchanged and the result is bit-identical.
63
+ max_block_bytes = 64 << 20
64
+
65
+ def __init__(self, mul, certify: bool = True):
66
  self.mul_lut = build_mul8_lut(mul)
67
+ if certify:
68
+ ok, tot = certify_mul8_lut(self.mul_lut)
69
+ if ok != tot:
70
+ raise ValueError(
71
+ "verified multiply LUT is not bit-exact: %d/%d entries match "
72
+ "signed integer multiply" % (ok, tot))
73
+ self.certified = (ok, tot)
74
 
75
  def available(self):
76
  return True
 
78
  def gemm(self, A: np.ndarray, B: np.ndarray) -> np.ndarray:
79
  au = (A.astype(np.int64) & 0xFF)
80
  bu = (B.astype(np.int64) & 0xFF)
81
+ m, k = au.shape
82
+ n = bu.shape[1]
83
  from . import instrument
84
  instrument.bump("VerifiedMul(LUT).gemms", 1)
85
+ instrument.bump("VerifiedMul(LUT).products", m * n * k)
86
+ # products via table lookup, then sum over the contraction axis
87
+ bt = bu.T[None, :, :]
88
+ rows = max(1, int(self.max_block_bytes // max(1, n * k * 8)))
89
+ if rows >= m: # small layer: one block
90
+ return self.mul_lut[au[:, None, :], bt].sum(axis=2)
91
+ out = np.empty((m, n), dtype=np.int64)
92
+ for i in range(0, m, rows):
93
+ out[i:i + rows] = self.mul_lut[au[i:i + rows, None, :], bt].sum(axis=2)
94
+ return out
test_verified_units.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Full-domain regression tests for the paths that actually ship.
2
+
3
+ The units already carry exhaustive verification of their SCALAR entry points
4
+ (`NeuralMul8.verify()` walks all 65536 signed pairs against `a*b`). Production
5
+ does not call those. It calls the BATCHED paths -- `mul_array`, `relu_array`,
6
+ `requant_array` -- and the lookup tables built from them. Those are different
7
+ code: nibble concatenation, one batched forward, vectorized sign correction.
8
+
9
+ A guarantee that covers a path nobody runs is not the guarantee anyone wanted,
10
+ so every check here goes against a golden reference (Python integer arithmetic),
11
+ over the complete finite domain, on the path that ships.
12
+
13
+ Run: python test_verified_units.py
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import sys
18
+
19
+ import numpy as np
20
+
21
+ from daisychain.verified.qat import load_units, build_luts
22
+ from daisychain.verified import instrument
23
+ from daisychain.verified.kernel import gemm_int8, MAX_INT32_K
24
+ from daisychain.verified.backends import NeuralBackend
25
+ from daisychain.verified.lut import certify_mul8_lut
26
+
27
+ FAILURES = []
28
+
29
+
30
+ def ck(name, cond, detail=""):
31
+ print(" %-4s %s%s" % ("ok" if cond else "FAIL", name,
32
+ "" if cond else " <- " + str(detail)))
33
+ if not cond:
34
+ FAILURES.append(name)
35
+
36
+
37
+ def signed_domain():
38
+ """Every ordered pair of signed bytes, and their true products."""
39
+ a = np.repeat(np.arange(-128, 128), 256)
40
+ b = np.tile(np.arange(-128, 128), 256)
41
+ return a, b, a.astype(np.int64) * b.astype(np.int64)
42
+
43
+
44
+ def main():
45
+ units = load_units()
46
+ mul, requant, relu_unit = units
47
+ luts = build_luts(*units)
48
+ backend = luts["backend"]
49
+
50
+ print("multiply -- the SHIPPED batched path, full domain")
51
+ a, b, gold = signed_domain()
52
+ got = mul.mul_array(a.astype(np.int8), b.astype(np.int8))
53
+ ck("mul_array == a*b over all 65536 signed pairs", np.array_equal(got, gold))
54
+
55
+ print("multiply -- the materialized table")
56
+ ok, tot = certify_mul8_lut(backend.mul_lut)
57
+ ck("mul LUT == a*b over all 65536 entries", ok == tot, "%d/%d" % (ok, tot))
58
+ ck("LUTBackend self-certified at construction",
59
+ getattr(backend, "certified", None) == (65536, 65536))
60
+
61
+ print("requantize -- sat_int8(x >> 8), full int16 domain")
62
+ x = np.arange(65536)
63
+ xs = np.where(x >= 32768, x - 65536, x)
64
+ rq_gold = np.clip(xs >> requant.shift, -128, 127)
65
+ ck("requant_array == sat_int8(x >> shift) over all 65536",
66
+ np.array_equal(requant.requant_array(x), rq_gold))
67
+ ck("requant LUT == golden over all 65536",
68
+ np.array_equal(luts["requant"][x & 0xFFFF], rq_gold))
69
+
70
+ print("relu -- max(0, x), full int8 domain")
71
+ r = np.arange(256)
72
+ rs = np.where(r >= 128, r - 256, r)
73
+ relu_gold = np.maximum(0, rs)
74
+ ck("relu_array == max(0,x) over all 256",
75
+ np.array_equal(relu_unit.relu_array(rs.astype(np.int8)), relu_gold))
76
+ ck("relu LUT == golden over all 256",
77
+ np.array_equal(luts["relu"][r & 0xFF], relu_gold))
78
+
79
+ print("GEMM -- blocked paths must be bit-identical to golden integer matmul")
80
+ rng = np.random.default_rng(11)
81
+ for (m, k, n) in [(1, 1, 1), (3, 5, 7), (64, 64, 64), (96, 128, 96), (129, 257, 65)]:
82
+ A = rng.integers(-128, 128, size=(m, k), dtype=np.int16).astype(np.int8)
83
+ B = rng.integers(-128, 128, size=(k, n), dtype=np.int16).astype(np.int8)
84
+ g = A.astype(np.int64) @ B.astype(np.int64)
85
+ ck("LUT gemm %dx%dx%d == int64 matmul" % (m, k, n),
86
+ np.array_equal(backend.gemm(A, B), g))
87
+
88
+ # The neural backend is the slow functional path; check it on a small shape
89
+ # AND across a block boundary, since blocking is what this change introduced.
90
+ # The block count is ASSERTED rather than assumed: mul_array bumps
91
+ # NeuralMul4.forward_calls once per call, i.e. once per block, so the counter
92
+ # is direct evidence the split actually happened. A test that says
93
+ # "forced multi-block" while silently running one block proves nothing.
94
+ nb = NeuralBackend(mul)
95
+ nb.max_products = 16 # force many blocks on a tiny GEMM
96
+ A = rng.integers(-128, 128, size=(17, 9), dtype=np.int16).astype(np.int8)
97
+ B = rng.integers(-128, 128, size=(9, 11), dtype=np.int16).astype(np.int8)
98
+ instrument.enable()
99
+ instrument.reset()
100
+ got_nb = nb.gemm(A, B)
101
+ blocks = instrument.report().get("NeuralMul4.forward_calls", 0)
102
+ instrument.disable()
103
+ ck("neural gemm actually split into >1 block", blocks > 1, "blocks=%d" % blocks)
104
+ ck("neural gemm == int64 matmul (multi-block)",
105
+ np.array_equal(got_nb, A.astype(np.int64) @ B.astype(np.int64)))
106
+
107
+ print("GEMM -- blocking must not change results as the block size varies")
108
+ A = rng.integers(-128, 128, size=(40, 24), dtype=np.int16).astype(np.int8)
109
+ B = rng.integers(-128, 128, size=(24, 32), dtype=np.int16).astype(np.int8)
110
+ ref = backend.gemm(A, B)
111
+ same = True
112
+ for cap in (1 << 10, 1 << 14, 1 << 18, 1 << 26):
113
+ backend.max_block_bytes = cap
114
+ same &= bool(np.array_equal(backend.gemm(A, B), ref))
115
+ backend.max_block_bytes = 64 << 20
116
+ ck("identical across block sizes 1 KB .. 64 MB", same)
117
+
118
+ print("int32 accumulator bound is enforced, not assumed")
119
+ ck("MAX_INT32_K == 131071", MAX_INT32_K == 131071, MAX_INT32_K)
120
+ small = np.zeros((1, 4), dtype=np.int8)
121
+ ck("gemm_int8 accepts K within bound",
122
+ gemm_int8(small, np.zeros((4, 1), dtype=np.int8)).shape == (1, 1))
123
+ try:
124
+ gemm_int8(np.zeros((1, MAX_INT32_K + 1), dtype=np.int8),
125
+ np.zeros((MAX_INT32_K + 1, 1), dtype=np.int8))
126
+ ck("gemm_int8 rejects K past the bound", False, "no error raised")
127
+ except ValueError:
128
+ ck("gemm_int8 rejects K past the bound", True)
129
+ except MemoryError:
130
+ ck("gemm_int8 rejects K past the bound", False,
131
+ "allocated before checking -- bound must be checked first")
132
+
133
+ print("instrument -- a zero must not be able to masquerade as evidence")
134
+ instrument.disable()
135
+ instrument.reset()
136
+ ck("enabled() reports the probe state", instrument.enabled() is False)
137
+ try:
138
+ instrument.require(**{"NeuralMul4.forward_calls": 1})
139
+ ck("require() refuses to report while disabled", False, "returned instead")
140
+ except RuntimeError:
141
+ ck("require() refuses to report while disabled", True)
142
+
143
+ instrument.enable()
144
+ instrument.reset()
145
+ backend.gemm(np.ones((2, 2), dtype=np.int8), np.ones((2, 2), dtype=np.int8))
146
+ try:
147
+ instrument.require(**{"VerifiedMul(LUT).gemms": 1})
148
+ ck("require() passes when the unit actually ran", True)
149
+ except AssertionError as e:
150
+ ck("require() passes when the unit actually ran", False, e)
151
+ try:
152
+ instrument.require(**{"VerifiedMul(LUT).gemms": 10 ** 9})
153
+ ck("require() fails when a unit is under-invoked", False, "did not raise")
154
+ except AssertionError:
155
+ ck("require() fails when a unit is under-invoked", True)
156
+ instrument.disable()
157
+
158
+ print()
159
+ if FAILURES:
160
+ print("FAILED: %d" % len(FAILURES))
161
+ for f in FAILURES:
162
+ print(" - %s" % f)
163
+ return 1
164
+ print("all checks passed")
165
+ return 0
166
+
167
+
168
+ if __name__ == "__main__":
169
+ sys.exit(main())