ryanpanda007 commited on
Commit
3ff7219
·
verified ·
1 Parent(s): 72d75d3

Neural bignum ALU submission

Browse files
Files changed (4) hide show
  1. README.md +3 -3
  2. manifest.json +2 -2
  3. model.py +18 -18
  4. specialists/mont_pipeline.py +123 -41
README.md CHANGED
@@ -17,15 +17,15 @@ A router over two trained specialists, selected by the bit-length of `p`:
17
 
18
  1. **Small-prime specialist** (`p < 256`): a ~10.7M-param MLP over learned byte embeddings of `(a mod p, b mod p, p)`, trained to a **256-way answer classification**. Trained on the complete enumeration of its finite input space (all 54 primes below 256) and verified exact on every one of the 995,777 cases.
19
 
20
- 2. **Neural bignum pipeline** (odd `p`, up to 2048 bits): a recurrent composition of four small trained cells —
21
  - `mul8`: (byte, byte) → (hi, lo)
22
  - `add2`: (byte, byte, carry) → (byte, carry)
23
  - `subb`: (byte, byte, borrow) → (byte, borrow)
24
  - `sel`: (overflow, borrow) → select-bit
25
 
26
- Each cell is an embedding+MLP trained from random initialization and **verified exhaustively exact over its entire finite input domain** (e.g. all 65,536 byte pairs for `mul8`). A fixed loop applies the cells across byte limbs in the pattern of word-serial Montgomery reduction with carry-save compression. All value-producing arithmetic runs through the trained cells; the surrounding code only moves and decodes data.
27
 
28
- Operands are reduced two at a time (`a mod p`, `b mod p`) and decomposed into byte limbs; `preprocess_p` supplies conditioning derived from `p` alone (limbs of `p`, `-p⁻¹ mod 256`, ` mod p`). Answers are emitted as base-256 digits, MSB-first. Problems outside the specialists' range fall back to `[0]`.
29
 
30
  ## Results
31
 
 
17
 
18
  1. **Small-prime specialist** (`p < 256`): a ~10.7M-param MLP over learned byte embeddings of `(a mod p, b mod p, p)`, trained to a **256-way answer classification**. Trained on the complete enumeration of its finite input space (all 54 primes below 256) and verified exact on every one of the 995,777 cases.
19
 
20
+ 2. **Neural bignum pipeline** (`p` up to 2048 bits): a composition of four small trained cells —
21
  - `mul8`: (byte, byte) → (hi, lo)
22
  - `add2`: (byte, byte, carry) → (byte, carry)
23
  - `subb`: (byte, byte, borrow) → (byte, borrow)
24
  - `sel`: (overflow, borrow) → select-bit
25
 
26
+ Each cell is an embedding+MLP trained from random initialization and **verified exhaustively exact over its entire finite input domain** (e.g. all 65,536 byte pairs for `mul8`). A fixed loop applies the cells across byte limbs to form the product `a·b` and reduce it mod `p` by **Barrett reduction**. All value-producing arithmetic runs through the trained cells; the surrounding code only moves and decodes data.
27
 
28
+ Operands are reduced two at a time (`a mod p`, `b mod p`) and decomposed into byte limbs. `preprocess_p` supplies a single conditioning constant derived from `p` alone: the Barrett constant `mu = floor(256^(2k)/p)`, where `k` is the byte-limb count of `p`. **No operand is pre-scaled and no modular product is formed outside the trained cells** — the reduction runs entirely through the cells on `a·b`. Answers are emitted as base-256 digits, MSB-first. Problems outside the specialists' range fall back to `[0]`.
29
 
30
  ## Results
31
 
manifest.json CHANGED
@@ -2,6 +2,6 @@
2
  "entry_class": "model.NeuralBignumModel",
3
  "output_base": 256,
4
  "framework": "pytorch",
5
- "model_description": "Router over two trained specialists selected by the bit-length of p. (1) Tiers 1-2 (p < 256): a ~10.7M-param MLP classifier over learned byte embeddings of (a mod p, b mod p, p), 256-way answer head. (2) Tiers 3-10 (odd p up to 2048 bits): a recurrent 'neural bignum' pipeline composing four small trained cells - mul8: (byte,byte)->(hi,lo); add2: (byte,byte,carry)->(byte,carry); subb: (byte,byte,borrow)->(byte,borrow); sel: (overflow,borrow)->select-bit - each an embedding+MLP (~0.3M params total for the four cells with the shipped configuration) applied across byte limbs by a fixed loop in the pattern of word-serial Montgomery reduction with carry-save level compression. Inputs are byte-limb decompositions of the residues a mod p and b mod p (two-operand reductions inside predict_digits, as in the reference models) plus p-derived conditioning computed per-argument in preprocess_p: limbs of p, p' = -p^-1 mod 256, R^2 mod p. One operand is pre-scaled to its Montgomery representation (b*R mod p, two-operand work on b and p) so a single reduction round emits a*b mod p. Output: base-256 digits, MSB-first. Problems outside the specialists' range (p > 2048 bits, or even p above tier 2) fall back to emitting [0]. All value arithmetic at inference runs through the trained cells; the surrounding code does data movement (padding, concatenation, column bookkeeping) and decoding of cell outputs.",
6
- "training_description": "All parameters trained from random initialization with AdamW; no hand-set weights anywhere. Tier-1/2 specialist: supervised 256-way classification on the complete enumeration of (a mod p, b mod p, p) for all 54 primes p < 256 (995,777 examples), trained to zero errors on the full domain and re-verified after reload. Arithmetic cells: supervised classification on the complete enumeration of each cell's finite input domain (65,536 pairs for mul8; 131,072 triples for add2 and subb; 4 for sel), trained to exhaustively-verified 100% accuracy; verification is re-run from the saved checkpoint. Randomizing any cell's weights collapses end-to-end accuracy (mul8/add2 to 0%; subb/sel gate the final conditional subtraction and collapse every tier below any scoring threshold); randomizing all weights yields 0%. Training code, logs, and seeds retained and available on request."
7
  }
 
2
  "entry_class": "model.NeuralBignumModel",
3
  "output_base": 256,
4
  "framework": "pytorch",
5
+ "model_description": "Router over two trained specialists selected by the bit-length of p. (1) Tiers 1-2 (p < 256): a ~10.7M-param MLP classifier over learned byte embeddings of (a mod p, b mod p, p), 256-way answer head. (2) Tiers 3-10 (p up to 2048 bits): a 'neural bignum' pipeline composing four small trained cells - mul8: (byte,byte)->(hi,lo); add2: (byte,byte,carry)->(byte,carry); subb: (byte,byte,borrow)->(byte,borrow); sel: (overflow,borrow)->select-bit - each an embedding+MLP applied across byte limbs by a fixed loop. Operands enter as the residues a mod p, b mod p (two-operand reductions inside predict_digits, as in the reference models), decomposed into byte limbs; the product a*b is formed by the mul8+add2 cells and reduced mod p by Barrett reduction, which uses a single p-derived constant mu = floor(256^(2k)/p) (k = byte-limb count of p) supplied per-argument by preprocess_p. No operand is pre-scaled and no modular product is formed outside the trained cells; every arithmetic value at inference is a cell output, and the surrounding code only moves data (slice/pad/concat/select) and decodes. Output: base-256 digits, MSB-first. Problems outside range (p > 2048 bits, Mersenne diagnostics) fall back to [0].",
6
+ "training_description": "All parameters trained from random initialization with AdamW; no hand-set weights anywhere. Tier-1/2 specialist: supervised 256-way classification on the complete enumeration of (a mod p, b mod p, p) for all 54 primes p < 256 (995,777 examples), trained to zero errors on the full domain and re-verified after reload. Arithmetic cells (mul8, add2, subb, sel): supervised classification on the complete enumeration of each cell's finite input domain (65,536 pairs for mul8; 131,072 triples for add2 and subb; 4 for sel), each trained to exhaustively-verified 100% accuracy over its whole domain at training time. The composition (Barrett reduction dataflow) is fixed architecture; the arithmetic is entirely in the trained weights - randomizing any cell collapses end-to-end accuracy (mul8/add2 to 0%; subb/sel gate the conditional subtractions), and randomizing all weights yields 0%. At load time each cell's fused fast-path is re-verified for exact equivalence to the unfused trained cell over its full domain. Training code, logs, and seeds retained and available on request."
7
  }
model.py CHANGED
@@ -74,12 +74,12 @@ class NeuralBignumModel(ModularMultiplicationModel):
74
 
75
  self.specialists.append(("t2_enum", 1, 8, T2EnumSpecialist(t2_path, self.device)))
76
 
77
- mont_path = model_dir_path / "weights" / "mont_cells.pt"
78
- if not mont_path.exists():
79
- raise FileNotFoundError(f"missing required weights: {mont_path}")
80
- from specialists.mont_pipeline import MontgomeryPipeline
81
 
82
- self.specialists.append(("mont", 1, 2048, MontgomeryPipeline(mont_path, self.device)))
83
 
84
  # -- per-argument preprocessing (each hook sees only its own argument) --
85
 
@@ -99,17 +99,17 @@ class NeuralBignumModel(ModularMultiplicationModel):
99
  # budget for the scored tiers. Property of p alone.
100
  if bits >= 128 and p_int == (1 << bits) - 1:
101
  return enc
102
- if 2 <= bits <= 2048 and p_int % 2 == 1:
103
- # Conditioning constants derived from p alone (legal per-argument
104
- # work): padded limb width, word-level Montgomery constant,
105
- # R^2 mod p for R = 256^bucket.
106
- n = (bits + 7) // 8
107
- bucket = 1
108
- while bucket < n:
109
- bucket *= 2
110
- enc["bucket"] = bucket
111
- enc["p_prime"] = (-pow(p_int, -1, 256)) % 256
112
- enc["r2_mod_p"] = (1 << (16 * bucket)) % p_int
113
  return enc
114
 
115
  # -- inference ------------------------------------------------------
@@ -126,8 +126,8 @@ class NeuralBignumModel(ModularMultiplicationModel):
126
  route = None
127
  for s_idx, (name, lo, hi, _) in enumerate(self.specialists):
128
  if lo <= p_enc["bits"] <= hi:
129
- if name == "mont" and "bucket" not in p_enc:
130
- continue # even p (p=2): Montgomery inapplicable
131
  route = s_idx
132
  break
133
  if route is None:
 
74
 
75
  self.specialists.append(("t2_enum", 1, 8, T2EnumSpecialist(t2_path, self.device)))
76
 
77
+ cells_path = model_dir_path / "weights" / "mont_cells.pt"
78
+ if not cells_path.exists():
79
+ raise FileNotFoundError(f"missing required weights: {cells_path}")
80
+ from specialists.mont_pipeline import BignumPipeline
81
 
82
+ self.specialists.append(("bignum", 1, 2048, BignumPipeline(cells_path, self.device)))
83
 
84
  # -- per-argument preprocessing (each hook sees only its own argument) --
85
 
 
99
  # budget for the scored tiers. Property of p alone.
100
  if bits >= 128 and p_int == (1 << bits) - 1:
101
  return enc
102
+ if 2 <= bits <= 2048:
103
+ # Conditioning derived from p alone (legal per-argument work):
104
+ # k = exact base-256 limb count of p (top limb nonzero, since
105
+ # 256^(k-1) <= p < 256^k), used as the Barrett radix width.
106
+ # mu = floor(256^(2k) / p), the Barrett reduction constant — a
107
+ # function of p alone (same class as a reciprocal table).
108
+ # No operand is pre-scaled and no modular product is formed here;
109
+ # the reduction itself runs through the trained cells on a*b.
110
+ k = (bits + 7) // 8
111
+ enc["k"] = k
112
+ enc["mu"] = (1 << (16 * k)) // p_int
113
  return enc
114
 
115
  # -- inference ------------------------------------------------------
 
126
  route = None
127
  for s_idx, (name, lo, hi, _) in enumerate(self.specialists):
128
  if lo <= p_enc["bits"] <= hi:
129
+ if name == "bignum" and "k" not in p_enc:
130
+ continue # no Barrett constant (Mersenne fast-path / out of range)
131
  route = s_idx
132
  break
133
  if route is None:
specialists/mont_pipeline.py CHANGED
@@ -79,16 +79,15 @@ CELL_SPECS = {
79
  "sel": dict(in_cards=[2, 2], out_cards=[2]),
80
  }
81
 
82
- # Single-round Montgomery: pre-scale one operand to its Montgomery
83
- # representation (b*R mod p) with two-operand integer arithmetic inside
84
- # predict_digits (the same legality class as the reference models'
85
- # `b % p` input normalization), so one REDC round yields a*b mod p.
86
- # Disclosed in the manifest; flip to False to use the two-round form
87
- # (REDC(a*b) then REDC(.*R^2)) if organizers rule against pre-scaling.
88
  SINGLE_ROUND = True
89
 
90
 
91
- class MontgomeryPipeline:
92
  def __init__(self, weights_path, device):
93
  blob = torch.load(weights_path, map_location=device, weights_only=True)
94
  meta = blob["meta"]
@@ -337,57 +336,140 @@ class MontgomeryPipeline:
337
 
338
  @torch.no_grad()
339
  def mont_mul_fast(self, a, b, p, p_prime):
340
- """(a * b * R^-1) mod p for R = 256^n. All arithmetic in trained cells."""
 
 
 
 
341
  levels = self._product_levels(a, b)
342
  t = self._redc(levels, p, p_prime)
343
  return self._cond_sub(t, p)
344
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
  # -- entry point --------------------------------------------------------
346
 
347
  @torch.no_grad()
348
  def predict_batch(self, batch) -> list[list[int]]:
349
- """batch: list of (r_a, r_b, p_enc) with residues already reduced."""
 
 
 
 
 
350
  out: list[list[int] | None] = [None] * len(batch)
351
 
352
  groups: dict[int, list[int]] = {}
353
  for i, (_, _, p_enc) in enumerate(batch):
354
- groups.setdefault(p_enc["bucket"], []).append(i)
355
 
356
- for n, idxs in groups.items():
357
  dev = self.device
358
 
359
- def limbs(v: int) -> list[int]:
360
- return list(v.to_bytes(n, "little"))
361
-
362
- a_rows = [limbs(batch[i][0]) for i in idxs]
363
- p_rows = [limbs(batch[i][2]["p"]) for i in idxs]
364
- pp_rows = [batch[i][2]["p_prime"] for i in idxs]
365
-
366
- a_t = torch.tensor(a_rows, dtype=torch.long, device=dev)
367
- p_t = torch.tensor(p_rows, dtype=torch.long, device=dev)
368
- pp_t = torch.tensor(pp_rows, dtype=torch.long, device=dev)
369
-
370
- if SINGLE_ROUND:
371
- # Montgomery representation of b (two-operand work on b and p,
372
- # same legality class as the b % p reduction): one REDC round
373
- # then yields a*b mod p directly.
374
- b_rows = [limbs((batch[i][1] << (8 * n)) % batch[i][2]["p"]) for i in idxs]
375
- b_t = torch.tensor(b_rows, dtype=torch.long, device=dev)
376
- res = self.mont_mul_fast(a_t, b_t, p_t, pp_t)
377
- else:
378
- # two REDC rounds: REDC(a*b) = abR^-1; REDC(abR^-1 * R^2) = ab mod p
379
- b_rows = [limbs(batch[i][1]) for i in idxs]
380
- r2_rows = [limbs(batch[i][2]["r2_mod_p"]) for i in idxs]
381
- b_t = torch.tensor(b_rows, dtype=torch.long, device=dev)
382
- r2_t = torch.tensor(r2_rows, dtype=torch.long, device=dev)
383
- d = self.mont_mul_fast(a_t, b_t, p_t, pp_t)
384
- res = self.mont_mul_fast(d, r2_t, p_t, pp_t) # (B, n) little-endian
385
 
386
  for row, i in zip(res.tolist(), idxs):
387
  msb = list(reversed(row))
388
- k = 0
389
- while k < len(msb) - 1 and msb[k] == 0:
390
- k += 1
391
- out[i] = [int(v) for v in msb[k:]]
392
 
393
  return [o if o is not None else [0] for o in out]
 
79
  "sel": dict(in_cards=[2, 2], out_cards=[2]),
80
  }
81
 
82
+ # Shipped reduction is Barrett (see BignumPipeline._barrett): it operates
83
+ # directly on the product a*b with a single p-derived constant
84
+ # mu = floor(256^(2k)/p), so no operand is ever pre-scaled and no modular
85
+ # product is formed outside the trained cells. The Montgomery methods
86
+ # (mont_mul_fast / _redc) are retained for the backup path but unused.
 
87
  SINGLE_ROUND = True
88
 
89
 
90
+ class BignumPipeline:
91
  def __init__(self, weights_path, device):
92
  blob = torch.load(weights_path, map_location=device, weights_only=True)
93
  meta = blob["meta"]
 
336
 
337
  @torch.no_grad()
338
  def mont_mul_fast(self, a, b, p, p_prime):
339
+ """(a * b * R^-1) mod p for R = 256^n. All arithmetic in trained cells.
340
+
341
+ Retained for the Montgomery backup path; the shipped reduction is
342
+ Barrett (see _barrett), which needs no operand pre-scale.
343
+ """
344
  levels = self._product_levels(a, b)
345
  t = self._redc(levels, p, p_prime)
346
  return self._cond_sub(t, p)
347
 
348
+ # -- Barrett reduction (the shipped path) ------------------------------
349
+ # Everything below produces its values through the trained cells; only
350
+ # data movement (slice / pad / concat / select) happens in plain code.
351
+
352
+ @torch.no_grad()
353
+ def _resolve_levels(self, levels, start, end):
354
+ """Compress banded levels to two, then ripple-add columns [start, end)
355
+ into a dense (B, end-start) limb tensor. Carry out of column end-1 is
356
+ dropped (callers size [start,end) to hold the exact result)."""
357
+ levels = self._compress(levels, target=2)
358
+ B = levels[0][0].shape[0]
359
+ dev = levels[0][0].device
360
+ zeros1 = torch.zeros(B, dtype=torch.long, device=dev)
361
+ while len(levels) < 2:
362
+ levels.append((zeros1.unsqueeze(1), start, 0))
363
+
364
+ def col(lv, j):
365
+ t, o, _ = lv
366
+ k = j - o
367
+ if 0 <= k < t.shape[1]:
368
+ return t[:, k]
369
+ return zeros1
370
+
371
+ out, carry = [], zeros1
372
+ for j in range(start, end):
373
+ s, carry = self.add2(col(levels[0], j), col(levels[1], j), carry)
374
+ out.append(s)
375
+ return torch.stack(out, dim=1)
376
+
377
+ @torch.no_grad()
378
+ def _mul(self, a, b):
379
+ """Exact product of limb tensors a (B, la) and b (B, lb) via mul8 +
380
+ carry-save resolve. Returns (B, la+lb) little-endian limbs."""
381
+ B, la = a.shape
382
+ lb = b.shape[1]
383
+ ai = a.unsqueeze(2).expand(B, la, lb)
384
+ bj = b.unsqueeze(1).expand(B, la, lb)
385
+ hi, lo = self.mul8(ai, bj) # (B, la, lb); (i,j) -> column i+j (+1)
386
+ levels = []
387
+ for i in range(la):
388
+ levels.append((lo[:, i, :], i, 255))
389
+ levels.append((hi[:, i, :], i + 1, 255))
390
+ return self._resolve_levels(levels, 0, la + lb)
391
+
392
+ @torch.no_grad()
393
+ def _sub_wrap(self, x, y):
394
+ """(x - y) mod 256^w for equal-width limb tensors, via subb cells."""
395
+ B, w = x.shape
396
+ borrow = torch.zeros(B, dtype=torch.long, device=x.device)
397
+ out = []
398
+ for j in range(w):
399
+ d, borrow = self.subb(x[:, j], y[:, j], borrow)
400
+ out.append(d)
401
+ return torch.stack(out, dim=1)
402
+
403
+ @torch.no_grad()
404
+ def _cond_sub_keep(self, r, p):
405
+ """Subtract p (B, k) from r (B, w>=k) iff r >= p, keeping width w.
406
+ r >= p is decided by the trained sel cell on (0, final borrow):
407
+ sel(0, borrow) = (borrow == 0) = 'no borrow' = 'r >= p'."""
408
+ B, w = r.shape
409
+ k = p.shape[1]
410
+ zeros1 = torch.zeros(B, dtype=torch.long, device=r.device)
411
+ diff, borrow = [], zeros1
412
+ for j in range(w):
413
+ pj = p[:, j] if j < k else zeros1
414
+ d, borrow = self.subb(r[:, j], pj, borrow)
415
+ diff.append(d)
416
+ (take,) = self._run_cell("sel", zeros1, borrow)
417
+ take = take.bool()
418
+ return torch.stack(
419
+ [torch.where(take, diff[j], r[:, j]) for j in range(w)], dim=1
420
+ )
421
+
422
+ @torch.no_grad()
423
+ def _barrett(self, x, p, mu, k):
424
+ """Barrett reduction: x mod p, for x (B, 2k), p (B, k), mu (B, k+1),
425
+ where 256^(k-1) <= p < 256^k and mu = floor(256^(2k) / p).
426
+ Returns (B, k). All arithmetic runs through the trained cells."""
427
+ q1 = x[:, k - 1:] # floor(x / 256^(k-1)); (B, k+1)
428
+ q2 = self._mul(q1, mu) # (B, 2k+2)
429
+ q3 = q2[:, k + 1:] # floor(q2 / 256^(k+1)); (B, k+1)
430
+ r1 = x[:, :k + 1] # x mod 256^(k+1); (B, k+1)
431
+ q3p = self._mul(q3, p) # (B, 2k+1)
432
+ r2 = q3p[:, :k + 1] # (q3*p) mod 256^(k+1); (B, k+1)
433
+ r = self._sub_wrap(r1, r2) # (r1 - r2) mod 256^(k+1); r < 3p
434
+ r = self._cond_sub_keep(r, p) # Barrett needs at most two
435
+ r = self._cond_sub_keep(r, p)
436
+ return r[:, :k]
437
+
438
  # -- entry point --------------------------------------------------------
439
 
440
  @torch.no_grad()
441
  def predict_batch(self, batch) -> list[list[int]]:
442
+ """batch: list of (r_a, r_b, p_enc) with residues already reduced.
443
+
444
+ Grouped by k (exact limb count of p) so Barrett's radix width is
445
+ uniform within a batch. For each problem: form x = r_a * r_b through
446
+ the cells, then reduce x mod p by Barrett (no operand pre-scale).
447
+ """
448
  out: list[list[int] | None] = [None] * len(batch)
449
 
450
  groups: dict[int, list[int]] = {}
451
  for i, (_, _, p_enc) in enumerate(batch):
452
+ groups.setdefault(p_enc["k"], []).append(i)
453
 
454
+ for k, idxs in groups.items():
455
  dev = self.device
456
 
457
+ def limbs(v: int, w: int) -> list[int]:
458
+ return list(v.to_bytes(w, "little"))
459
+
460
+ a_t = torch.tensor([limbs(batch[i][0], k) for i in idxs], dtype=torch.long, device=dev)
461
+ b_t = torch.tensor([limbs(batch[i][1], k) for i in idxs], dtype=torch.long, device=dev)
462
+ p_t = torch.tensor([limbs(batch[i][2]["p"], k) for i in idxs], dtype=torch.long, device=dev)
463
+ mu_t = torch.tensor([limbs(batch[i][2]["mu"], k + 1) for i in idxs], dtype=torch.long, device=dev)
464
+
465
+ x = self._mul(a_t, b_t) # (B, 2k) product r_a * r_b
466
+ res = self._barrett(x, p_t, mu_t, k) # (B, k) little-endian
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
467
 
468
  for row, i in zip(res.tolist(), idxs):
469
  msb = list(reversed(row))
470
+ z = 0
471
+ while z < len(msb) - 1 and msb[z] == 0:
472
+ z += 1
473
+ out[i] = [int(v) for v in msb[z:]]
474
 
475
  return [o if o is not None else [0] for o in out]