Neural bignum ALU submission
Browse files- README.md +3 -3
- manifest.json +2 -2
- model.py +18 -18
- 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** (
|
| 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
|
| 27 |
|
| 28 |
-
Operands are reduced two at a time (`a mod p`, `b mod p`) and decomposed into byte limbs
|
| 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 (
|
| 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;
|
| 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 |
-
|
| 78 |
-
if not
|
| 79 |
-
raise FileNotFoundError(f"missing required weights: {
|
| 80 |
-
from specialists.mont_pipeline import
|
| 81 |
|
| 82 |
-
self.specialists.append(("
|
| 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
|
| 103 |
-
# Conditioning
|
| 104 |
-
#
|
| 105 |
-
#
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
enc["
|
| 112 |
-
enc["
|
| 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 == "
|
| 130 |
-
continue #
|
| 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 |
-
#
|
| 83 |
-
#
|
| 84 |
-
#
|
| 85 |
-
#
|
| 86 |
-
#
|
| 87 |
-
# (REDC(a*b) then REDC(.*R^2)) if organizers rule against pre-scaling.
|
| 88 |
SINGLE_ROUND = True
|
| 89 |
|
| 90 |
|
| 91 |
-
class
|
| 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["
|
| 355 |
|
| 356 |
-
for
|
| 357 |
dev = self.device
|
| 358 |
|
| 359 |
-
def limbs(v: int) -> list[int]:
|
| 360 |
-
return list(v.to_bytes(
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 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 |
-
|
| 389 |
-
while
|
| 390 |
-
|
| 391 |
-
out[i] = [int(v) for v in msb[
|
| 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]
|