Neural bignum ALU submission
Browse files- .gitignore +2 -0
- manifest.json +7 -0
- model.py +163 -0
- specialists/__init__.py +0 -0
- specialists/mont_pipeline.py +393 -0
- specialists/t2_enum.py +50 -0
- weights/mont_cells.pt +3 -0
- weights/t2_enum.pt +3 -0
.gitignore
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.pyc
|
manifest.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 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 |
+
}
|
model.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Router-based submission for the Modular Arithmetic Challenge.
|
| 2 |
+
|
| 3 |
+
Structure:
|
| 4 |
+
- ``preprocess_a`` / ``preprocess_b``: parse the decimal string to int (allowed
|
| 5 |
+
per-argument work).
|
| 6 |
+
- ``preprocess_p``: parse p and derive per-argument conditioning constants that
|
| 7 |
+
are functions of p alone (bit length, byte limbs, -p^-1 mod 256, R^2 mod p).
|
| 8 |
+
- ``predict_digits_batch``: legally reduces the operands (``a % p``, ``b % p`` --
|
| 9 |
+
the same two-operand reduction the reference models use; the three-argument
|
| 10 |
+
modular product is never computed in code), then routes each problem to a
|
| 11 |
+
trained specialist by the bit-length of p. Problems outside every
|
| 12 |
+
specialist's proven range emit the honest fallback ``[0]``.
|
| 13 |
+
|
| 14 |
+
Specialists register in ``SPECIALISTS`` (see ``load``). Each specialist gets
|
| 15 |
+
batched tensors of byte limbs and must return base-256 digit lists.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
|
| 22 |
+
from modchallenge.interface.base_model import ModularMultiplicationModel
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class NeuralBignumModel(ModularMultiplicationModel):
|
| 26 |
+
"""Entry class declared in manifest.json."""
|
| 27 |
+
|
| 28 |
+
def __init__(self) -> None:
|
| 29 |
+
self.device = None
|
| 30 |
+
self.specialists: list = [] # (name, min_p_bits, max_p_bits, module)
|
| 31 |
+
|
| 32 |
+
# -- lifecycle ------------------------------------------------------
|
| 33 |
+
|
| 34 |
+
def load(self, model_dir: str) -> None:
|
| 35 |
+
import os
|
| 36 |
+
|
| 37 |
+
import torch
|
| 38 |
+
|
| 39 |
+
# Match torch's CPU thread pool to the *effective* quota. In a
|
| 40 |
+
# container with a CFS quota (e.g. --cpus 4), torch defaults to the
|
| 41 |
+
# host's visible core count and oversubscribes badly on the many
|
| 42 |
+
# small matmuls this pipeline issues.
|
| 43 |
+
def _effective_cpus() -> int:
|
| 44 |
+
try:
|
| 45 |
+
parts = open("/sys/fs/cgroup/cpu.max").read().split()
|
| 46 |
+
if parts[0] != "max":
|
| 47 |
+
return max(1, int(parts[0]) // int(parts[1]))
|
| 48 |
+
except OSError:
|
| 49 |
+
pass
|
| 50 |
+
try:
|
| 51 |
+
return len(os.sched_getaffinity(0))
|
| 52 |
+
except AttributeError:
|
| 53 |
+
return os.cpu_count() or 1
|
| 54 |
+
|
| 55 |
+
torch.set_num_threads(_effective_cpus())
|
| 56 |
+
|
| 57 |
+
if torch.cuda.is_available():
|
| 58 |
+
self.device = torch.device("cuda")
|
| 59 |
+
elif torch.backends.mps.is_available():
|
| 60 |
+
self.device = torch.device("mps")
|
| 61 |
+
else:
|
| 62 |
+
self.device = torch.device("cpu")
|
| 63 |
+
|
| 64 |
+
model_dir_path = Path(model_dir)
|
| 65 |
+
self.specialists = []
|
| 66 |
+
|
| 67 |
+
# Both weight files ship with the submission. Fail LOUDLY here if one
|
| 68 |
+
# is missing or corrupt — a silent capability downgrade at load time
|
| 69 |
+
# would zero whole tiers without any visible error.
|
| 70 |
+
t2_path = model_dir_path / "weights" / "t2_enum.pt"
|
| 71 |
+
if not t2_path.exists():
|
| 72 |
+
raise FileNotFoundError(f"missing required weights: {t2_path}")
|
| 73 |
+
from specialists.t2_enum import T2EnumSpecialist
|
| 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 |
+
|
| 86 |
+
def preprocess_a(self, a: str):
|
| 87 |
+
return int(a)
|
| 88 |
+
|
| 89 |
+
def preprocess_b(self, b: str):
|
| 90 |
+
return int(b)
|
| 91 |
+
|
| 92 |
+
def preprocess_p(self, p: str):
|
| 93 |
+
p_int = int(p)
|
| 94 |
+
bits = p_int.bit_length()
|
| 95 |
+
enc = {"p": p_int, "bits": bits}
|
| 96 |
+
# Mersenne moduli 2^k - 1 (k >= 128) appear only as tier-0 diagnostic
|
| 97 |
+
# primes (unscored); the chance a scored tier draws exactly a Mersenne
|
| 98 |
+
# is ~2^-500. Routing them to the fallback protects the shared time
|
| 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 ------------------------------------------------------
|
| 116 |
+
|
| 117 |
+
def predict_digits(self, a_enc, b_enc, p_enc) -> list[int]:
|
| 118 |
+
return self.predict_digits_batch([(a_enc, b_enc, p_enc)])[0]
|
| 119 |
+
|
| 120 |
+
def predict_digits_batch(self, inputs) -> list[list[int]]:
|
| 121 |
+
out: list[list[int] | None] = [None] * len(inputs)
|
| 122 |
+
|
| 123 |
+
# Group problem indices by matching specialist.
|
| 124 |
+
groups: dict[int, list[int]] = {i: [] for i in range(len(self.specialists))}
|
| 125 |
+
for i, (a_enc, b_enc, p_enc) in enumerate(inputs):
|
| 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:
|
| 134 |
+
out[i] = [0] # honest fallback: never learned this range
|
| 135 |
+
else:
|
| 136 |
+
groups[route].append(i)
|
| 137 |
+
|
| 138 |
+
for s_idx, idxs in groups.items():
|
| 139 |
+
if not idxs:
|
| 140 |
+
continue
|
| 141 |
+
_, _, _, spec = self.specialists[s_idx]
|
| 142 |
+
batch = []
|
| 143 |
+
for i in idxs:
|
| 144 |
+
a_enc, b_enc, p_enc = inputs[i]
|
| 145 |
+
p_int = p_enc["p"]
|
| 146 |
+
# Two-operand reduction (allowed; see module docstring).
|
| 147 |
+
batch.append((a_enc % p_int, b_enc % p_int, p_enc))
|
| 148 |
+
try:
|
| 149 |
+
preds = spec.predict_batch(batch)
|
| 150 |
+
if len(preds) != len(idxs):
|
| 151 |
+
raise RuntimeError("specialist violated batch contract")
|
| 152 |
+
except Exception:
|
| 153 |
+
# Containment: a failure (e.g. OOM) in one group must not
|
| 154 |
+
# abort the run or break the batch contract; those problems
|
| 155 |
+
# score 0 via the honest fallback and the rest survive.
|
| 156 |
+
preds = [[0]] * len(idxs)
|
| 157 |
+
for j, i in enumerate(idxs):
|
| 158 |
+
out[i] = preds[j]
|
| 159 |
+
|
| 160 |
+
return [o if o is not None else [0] for o in out]
|
| 161 |
+
|
| 162 |
+
def max_batch_size(self) -> int:
|
| 163 |
+
return 256
|
specialists/__init__.py
ADDED
|
File without changes
|
specialists/mont_pipeline.py
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Neural bignum pipeline: word-level Montgomery modmul through trained cells.
|
| 2 |
+
|
| 3 |
+
Every arithmetic mapping (8x8 multiply, add-with-carry, subtract-with-borrow)
|
| 4 |
+
is a small MLP trained from random init to exact accuracy on its finite
|
| 5 |
+
domain (training/train_cells.py) and verified exhaustively. This module holds
|
| 6 |
+
NO arithmetic on the problem values: it embeds bytes, runs the trained cells,
|
| 7 |
+
and moves data (pad/shift/concat/select). The composition pattern is
|
| 8 |
+
multiplication + word-serial Montgomery reduction (REDC); the function
|
| 9 |
+
executed at every step lives entirely in the trained weights (randomize any
|
| 10 |
+
cell's weights and accuracy collapses).
|
| 11 |
+
|
| 12 |
+
Representation: the accumulator is a set of LEVELS, each a (B, C) tensor of
|
| 13 |
+
byte values per column, plus a static per-level value bound used only for
|
| 14 |
+
data-independent shape scheduling (never for arithmetic). Adding two levels
|
| 15 |
+
through the add2 cell yields a sum level and a carry level shifted one column
|
| 16 |
+
up; levels with a provable bound of zero are dropped. All level pairs of a
|
| 17 |
+
compression pass run as ONE batched cell call.
|
| 18 |
+
|
| 19 |
+
Batching: problems are grouped by padded limb count (power-of-two buckets,
|
| 20 |
+
precomputed in preprocess_p together with R^2 mod p and p' = -p^-1 mod 256).
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
from __future__ import annotations
|
| 24 |
+
|
| 25 |
+
import torch
|
| 26 |
+
import torch.nn as nn
|
| 27 |
+
import torch.nn.functional as F
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class CellMLP(nn.Module):
|
| 31 |
+
"""Must match training/train_cells.py exactly."""
|
| 32 |
+
|
| 33 |
+
def __init__(self, in_cards, out_cards, emb: int, hidden: int):
|
| 34 |
+
super().__init__()
|
| 35 |
+
self.embs = nn.ModuleList([nn.Embedding(c, emb) for c in in_cards])
|
| 36 |
+
d_in = emb * len(in_cards)
|
| 37 |
+
self.trunk = nn.Sequential(
|
| 38 |
+
nn.Linear(d_in, hidden), nn.GELU(),
|
| 39 |
+
nn.Linear(hidden, hidden), nn.GELU(),
|
| 40 |
+
)
|
| 41 |
+
self.heads = nn.ModuleList([nn.Linear(hidden, c) for c in out_cards])
|
| 42 |
+
self.fused_tables: list[torch.Tensor] | None = None
|
| 43 |
+
self.fused_bias: torch.Tensor | None = None
|
| 44 |
+
|
| 45 |
+
def fuse(self) -> None:
|
| 46 |
+
"""Fold Embedding + first Linear into per-input lookup tables.
|
| 47 |
+
|
| 48 |
+
Exact algebraic identity on the trained weights (h1 = sum_i E_i[x_i] @
|
| 49 |
+
W1_i^T + b1), computed once at load time. Callers must re-verify the
|
| 50 |
+
fused cell exhaustively before trusting it (float reassociation could
|
| 51 |
+
in principle flip a near-tie argmax; verification makes that a no-op).
|
| 52 |
+
"""
|
| 53 |
+
lin1: nn.Linear = self.trunk[0]
|
| 54 |
+
emb = self.embs[0].weight.shape[1]
|
| 55 |
+
tables = []
|
| 56 |
+
for i, e in enumerate(self.embs):
|
| 57 |
+
w_slice = lin1.weight[:, i * emb:(i + 1) * emb] # (hidden, emb)
|
| 58 |
+
tables.append(e.weight @ w_slice.T) # (card_i, hidden)
|
| 59 |
+
self.fused_tables = tables
|
| 60 |
+
self.fused_bias = lin1.bias
|
| 61 |
+
|
| 62 |
+
def forward(self, *cols):
|
| 63 |
+
if self.fused_tables is not None:
|
| 64 |
+
h = self.fused_bias
|
| 65 |
+
for t, c in zip(self.fused_tables, cols):
|
| 66 |
+
h = h + t[c]
|
| 67 |
+
h = torch.nn.functional.gelu(h)
|
| 68 |
+
h = self.trunk[3](self.trunk[2](h)) # Linear2 + GELU
|
| 69 |
+
else:
|
| 70 |
+
h = torch.cat([e(c) for e, c in zip(self.embs, cols)], dim=-1)
|
| 71 |
+
h = self.trunk(h)
|
| 72 |
+
return [head(h).argmax(-1) for head in self.heads]
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
CELL_SPECS = {
|
| 76 |
+
"mul8": dict(in_cards=[256, 256], out_cards=[256, 256]),
|
| 77 |
+
"add2": dict(in_cards=[256, 256, 2], out_cards=[256, 2]),
|
| 78 |
+
"subb": dict(in_cards=[256, 256, 2], out_cards=[256, 2]),
|
| 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"]
|
| 95 |
+
self.cells: dict[str, CellMLP] = {}
|
| 96 |
+
for name, spec in CELL_SPECS.items():
|
| 97 |
+
# meta is either per-cell ({name: {emb, hidden}}) or legacy flat
|
| 98 |
+
dims = meta.get(name, meta) if isinstance(meta, dict) else meta
|
| 99 |
+
m = CellMLP(spec["in_cards"], spec["out_cards"], dims["emb"], dims["hidden"]).to(device)
|
| 100 |
+
m.load_state_dict(blob[name])
|
| 101 |
+
m.eval()
|
| 102 |
+
self._fuse_verified(m)
|
| 103 |
+
self.cells[name] = m
|
| 104 |
+
self.device = device
|
| 105 |
+
|
| 106 |
+
@staticmethod
|
| 107 |
+
@torch.no_grad()
|
| 108 |
+
def _fuse_verified(m: CellMLP) -> None:
|
| 109 |
+
"""Enable the fused (embedding+Linear1 folded) forward only if it
|
| 110 |
+
reproduces the unfused trained cell exactly over its FULL input
|
| 111 |
+
domain. Pure weight-view optimization; falls back to the original
|
| 112 |
+
forward on any mismatch."""
|
| 113 |
+
cards = [e.weight.shape[0] for e in m.embs]
|
| 114 |
+
grids = torch.meshgrid(
|
| 115 |
+
*[torch.arange(c, device=m.embs[0].weight.device) for c in cards],
|
| 116 |
+
indexing="ij",
|
| 117 |
+
)
|
| 118 |
+
cols = [g.reshape(-1) for g in grids]
|
| 119 |
+
n = cols[0].numel()
|
| 120 |
+
m.fused_tables = None
|
| 121 |
+
ref_chunks = []
|
| 122 |
+
for i in range(0, n, 262_144):
|
| 123 |
+
ref_chunks.append(m(*[c[i:i + 262_144] for c in cols]))
|
| 124 |
+
m.fuse()
|
| 125 |
+
for k, i in enumerate(range(0, n, 262_144)):
|
| 126 |
+
fused = m(*[c[i:i + 262_144] for c in cols])
|
| 127 |
+
for f_out, r_out in zip(fused, ref_chunks[k]):
|
| 128 |
+
if not torch.equal(f_out, r_out):
|
| 129 |
+
m.fused_tables = None
|
| 130 |
+
m.fused_bias = None
|
| 131 |
+
return
|
| 132 |
+
|
| 133 |
+
# -- batched cell invocations (flat long tensors of bytes) -----------
|
| 134 |
+
# Oversized flat calls are chunked: Metal/MPS rejects single GEMMs above
|
| 135 |
+
# a few million rows, and chunking also bounds peak memory on any device.
|
| 136 |
+
|
| 137 |
+
_CHUNK = 262_144 # keeps peak MLP activations well under 1 GB (8 GB sandbox)
|
| 138 |
+
|
| 139 |
+
def _run_cell(self, name: str, *cols):
|
| 140 |
+
shape = cols[0].shape
|
| 141 |
+
flat = [c.reshape(-1) for c in cols]
|
| 142 |
+
n = flat[0].numel()
|
| 143 |
+
if n <= self._CHUNK:
|
| 144 |
+
outs = self.cells[name](*flat)
|
| 145 |
+
else:
|
| 146 |
+
pieces = [
|
| 147 |
+
self.cells[name](*[f[i : i + self._CHUNK] for f in flat])
|
| 148 |
+
for i in range(0, n, self._CHUNK)
|
| 149 |
+
]
|
| 150 |
+
outs = [torch.cat([p[k] for p in pieces]) for k in range(len(pieces[0]))]
|
| 151 |
+
return [o.reshape(shape) for o in outs]
|
| 152 |
+
|
| 153 |
+
def mul8(self, x, y):
|
| 154 |
+
hi, lo = self._run_cell("mul8", x, y)
|
| 155 |
+
return hi, lo
|
| 156 |
+
|
| 157 |
+
def add2(self, x, y, cin):
|
| 158 |
+
s, c = self._run_cell("add2", x, y, cin)
|
| 159 |
+
return s, c
|
| 160 |
+
|
| 161 |
+
def subb(self, x, y, bin_):
|
| 162 |
+
d, b = self._run_cell("subb", x, y, bin_)
|
| 163 |
+
return d, b
|
| 164 |
+
|
| 165 |
+
# -- level algebra ----------------------------------------------------
|
| 166 |
+
# A level is (tensor (B, w), off:int, bound:int) covering columns
|
| 167 |
+
# [off, off+w). bound is a static upper bound on entries, used only to
|
| 168 |
+
# schedule shapes and pairing (data-independent, deterministic). The
|
| 169 |
+
# banded representation keeps cell calls sized to live content instead
|
| 170 |
+
# of the full column window.
|
| 171 |
+
|
| 172 |
+
def _pair_levels(self, levels):
|
| 173 |
+
"""One compression pass: pair levels via ONE batched add2 call.
|
| 174 |
+
|
| 175 |
+
Pairing order matters for convergence: first merge small levels whose
|
| 176 |
+
bounds sum <= 255 (no carry, count strictly decreases), then pair the
|
| 177 |
+
remaining large levels biggest-with-biggest (their carries are bound-1
|
| 178 |
+
levels that merge away on the next pass). Within each class, sort by
|
| 179 |
+
offset so paired bands overlap and unions stay tight.
|
| 180 |
+
"""
|
| 181 |
+
levels = sorted(levels, key=lambda lv: (lv[2], lv[1]))
|
| 182 |
+
pairs = []
|
| 183 |
+
rest = []
|
| 184 |
+
i = 0
|
| 185 |
+
while i + 1 < len(levels) and levels[i][2] + levels[i + 1][2] <= 255:
|
| 186 |
+
pairs.append((levels[i], levels[i + 1]))
|
| 187 |
+
i += 2
|
| 188 |
+
big = sorted(levels[i:], key=lambda lv: (-lv[2], lv[1]))
|
| 189 |
+
j = 0
|
| 190 |
+
while j + 1 < len(big):
|
| 191 |
+
pairs.append((big[j], big[j + 1]))
|
| 192 |
+
j += 2
|
| 193 |
+
if j < len(big):
|
| 194 |
+
rest.append(big[j])
|
| 195 |
+
|
| 196 |
+
if not pairs:
|
| 197 |
+
return levels
|
| 198 |
+
|
| 199 |
+
# pad each operand into its pair's union band, concat all pairs into
|
| 200 |
+
# one flat cell call, then split back by width
|
| 201 |
+
xs, ys, metas = [], [], []
|
| 202 |
+
for (ta, oa, ba), (tb, ob, bb) in pairs:
|
| 203 |
+
lo = min(oa, ob)
|
| 204 |
+
hi_ = max(oa + ta.shape[1], ob + tb.shape[1])
|
| 205 |
+
w = hi_ - lo
|
| 206 |
+
xs.append(F.pad(ta, (oa - lo, w - (oa - lo) - ta.shape[1])))
|
| 207 |
+
ys.append(F.pad(tb, (ob - lo, w - (ob - lo) - tb.shape[1])))
|
| 208 |
+
metas.append((lo, w, ba, bb))
|
| 209 |
+
x = torch.cat(xs, dim=1)
|
| 210 |
+
y = torch.cat(ys, dim=1)
|
| 211 |
+
zeros = torch.zeros_like(x)
|
| 212 |
+
s, c = self.add2(x, y, zeros)
|
| 213 |
+
|
| 214 |
+
out = list(rest)
|
| 215 |
+
pos = 0
|
| 216 |
+
for lo, w, ba, bb in metas:
|
| 217 |
+
s_k = s[:, pos:pos + w]
|
| 218 |
+
out.append((s_k, lo, min(ba + bb, 255)))
|
| 219 |
+
if ba + bb > 255:
|
| 220 |
+
# carry shifts one column up: same width, offset + 1
|
| 221 |
+
out.append((c[:, pos:pos + w], lo + 1, 1))
|
| 222 |
+
pos += w
|
| 223 |
+
return out
|
| 224 |
+
|
| 225 |
+
def _compress(self, levels, target: int):
|
| 226 |
+
guard = 0
|
| 227 |
+
while len(levels) > target:
|
| 228 |
+
levels = self._pair_levels(levels)
|
| 229 |
+
guard += 1
|
| 230 |
+
if guard > 100:
|
| 231 |
+
raise RuntimeError("compression did not converge")
|
| 232 |
+
return levels
|
| 233 |
+
|
| 234 |
+
def _resolve_chain(self, entries, zeros1):
|
| 235 |
+
"""Exactly sum a small list of (B,) bytes; returns (byte, carries)."""
|
| 236 |
+
acc = entries[0]
|
| 237 |
+
carries = []
|
| 238 |
+
for e in entries[1:]:
|
| 239 |
+
acc, c = self.add2(acc, e, torch.zeros_like(acc))
|
| 240 |
+
carries.append(c)
|
| 241 |
+
return acc, carries
|
| 242 |
+
|
| 243 |
+
# -- pipeline stages ---------------------------------------------------
|
| 244 |
+
|
| 245 |
+
@torch.no_grad()
|
| 246 |
+
def _product_levels(self, a, b):
|
| 247 |
+
"""Banded levels of the full product a*b (n limbs each). No padding:
|
| 248 |
+
row i of the partial-product grid is a level at offset i (lo) / i+1
|
| 249 |
+
(hi) — pure views of the mul8 output."""
|
| 250 |
+
B, n = a.shape
|
| 251 |
+
ai = a.unsqueeze(2).expand(B, n, n)
|
| 252 |
+
bj = b.unsqueeze(1).expand(B, n, n)
|
| 253 |
+
hi, lo = self.mul8(ai, bj) # (B, n, n); entry (i, j) -> column i+j (+1)
|
| 254 |
+
levels = []
|
| 255 |
+
for i in range(n):
|
| 256 |
+
levels.append((lo[:, i, :], i, 255))
|
| 257 |
+
levels.append((hi[:, i, :], i + 1, 255))
|
| 258 |
+
return levels
|
| 259 |
+
|
| 260 |
+
@staticmethod
|
| 261 |
+
def _trim_below(levels, base):
|
| 262 |
+
"""Slice off columns < base from every level (consumed/stale columns
|
| 263 |
+
must not remain: a later pairing could push their carries into live
|
| 264 |
+
columns). Drops levels that fall entirely below base."""
|
| 265 |
+
out = []
|
| 266 |
+
for t, o, b in levels:
|
| 267 |
+
if o >= base:
|
| 268 |
+
out.append((t, o, b))
|
| 269 |
+
elif o + t.shape[1] > base:
|
| 270 |
+
out.append((t[:, base - o:], base, b))
|
| 271 |
+
return out
|
| 272 |
+
|
| 273 |
+
@torch.no_grad()
|
| 274 |
+
def _redc(self, levels, p, p_prime):
|
| 275 |
+
"""Word-serial REDC by R = 256^n over banded level state.
|
| 276 |
+
Returns (B, n+1) resolved limbs (columns n..2n)."""
|
| 277 |
+
B, n = p.shape
|
| 278 |
+
zeros1 = torch.zeros(B, dtype=torch.long, device=p.device)
|
| 279 |
+
|
| 280 |
+
for base in range(n):
|
| 281 |
+
levels = self._compress(levels, target=2)
|
| 282 |
+
levels = self._trim_below(levels, base)
|
| 283 |
+
# resolve bottom column (absolute column = base)
|
| 284 |
+
col0 = [t[:, 0] for t, o, _ in levels if o == base and t.shape[1] > 0]
|
| 285 |
+
if not col0:
|
| 286 |
+
col0 = [zeros1]
|
| 287 |
+
t0, spill = self._resolve_chain(col0, zeros1)
|
| 288 |
+
# m = t0 * p' mod 256 (trained cell)
|
| 289 |
+
_, m = self.mul8(t0, p_prime)
|
| 290 |
+
hi, lo = self.mul8(m.unsqueeze(1).expand(B, n), p) # (B, n)
|
| 291 |
+
# bottom column: t0 + lo[:,0] == 0 mod 256; keep only its carry
|
| 292 |
+
_, c0 = self.add2(t0, lo[:, 0], zeros1)
|
| 293 |
+
inj = c0
|
| 294 |
+
for sp in spill:
|
| 295 |
+
inj, _ = self.add2(inj, sp, zeros1) # sums of 0/1 carries; no overflow
|
| 296 |
+
# consume column `base`: trim it from existing levels
|
| 297 |
+
levels = self._trim_below(levels, base + 1)
|
| 298 |
+
levels.append((inj.unsqueeze(1), base + 1, 8))
|
| 299 |
+
# m*p contributes lo[1:] at cols base+1..base+n-1, hi at base+1..base+n
|
| 300 |
+
levels.append((lo[:, 1:], base + 1, 255))
|
| 301 |
+
levels.append((hi, base + 1, 255))
|
| 302 |
+
|
| 303 |
+
# final: live content is columns n..2n; resolve exactly via ripple.
|
| 304 |
+
levels = self._compress(levels, target=2)
|
| 305 |
+
while len(levels) < 2:
|
| 306 |
+
levels.append((torch.zeros(B, 1, dtype=torch.long, device=p.device), n, 0))
|
| 307 |
+
|
| 308 |
+
def col(lv, j):
|
| 309 |
+
t, o, _ = lv
|
| 310 |
+
k = j - o
|
| 311 |
+
if 0 <= k < t.shape[1]:
|
| 312 |
+
return t[:, k]
|
| 313 |
+
return zeros1
|
| 314 |
+
|
| 315 |
+
out = []
|
| 316 |
+
carry = zeros1
|
| 317 |
+
for j in range(n, 2 * n + 1):
|
| 318 |
+
s, carry = self.add2(col(levels[0], j), col(levels[1], j), carry)
|
| 319 |
+
out.append(s)
|
| 320 |
+
return torch.stack(out, dim=1) # (B, n+1)
|
| 321 |
+
|
| 322 |
+
@torch.no_grad()
|
| 323 |
+
def _cond_sub(self, t, p):
|
| 324 |
+
"""t (B, n+1) minus p if t >= p, via trained subb cells + select."""
|
| 325 |
+
B, n = p.shape
|
| 326 |
+
zeros1 = torch.zeros(B, dtype=torch.long, device=p.device)
|
| 327 |
+
diff, borrow = [], zeros1
|
| 328 |
+
for j in range(n):
|
| 329 |
+
d, borrow = self.subb(t[:, j], p[:, j], borrow)
|
| 330 |
+
diff.append(d)
|
| 331 |
+
# selection bit comes from the trained sel cell: (overflow, borrow) -> take
|
| 332 |
+
(take,) = self._run_cell("sel", t[:, n], borrow)
|
| 333 |
+
take = take.bool()
|
| 334 |
+
return torch.stack(
|
| 335 |
+
[torch.where(take, diff[j], t[:, j]) for j in range(n)], dim=1
|
| 336 |
+
)
|
| 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]
|
specialists/t2_enum.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tier-1/2 specialist: trained classifier over the finite small-prime domain.
|
| 2 |
+
|
| 3 |
+
Weights are trained (training/train_t2_enum.py) from random init on the
|
| 4 |
+
complete enumeration of (a mod p, b mod p, p) for all primes < 256 and
|
| 5 |
+
verified exact on that full domain. At inference the network's argmax IS the
|
| 6 |
+
answer digit; there is no arithmetic here.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import torch
|
| 12 |
+
import torch.nn as nn
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class T2Net(nn.Module):
|
| 16 |
+
def __init__(self, d: int = 256, hidden: int = 2048):
|
| 17 |
+
super().__init__()
|
| 18 |
+
self.emb_a = nn.Embedding(256, d)
|
| 19 |
+
self.emb_b = nn.Embedding(256, d)
|
| 20 |
+
self.emb_p = nn.Embedding(256, d)
|
| 21 |
+
self.net = nn.Sequential(
|
| 22 |
+
nn.Linear(3 * d, hidden),
|
| 23 |
+
nn.GELU(),
|
| 24 |
+
nn.Linear(hidden, hidden),
|
| 25 |
+
nn.GELU(),
|
| 26 |
+
nn.Linear(hidden, hidden),
|
| 27 |
+
nn.GELU(),
|
| 28 |
+
nn.Linear(hidden, 256),
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
def forward(self, ra, rb, p):
|
| 32 |
+
h = torch.cat([self.emb_a(ra), self.emb_b(rb), self.emb_p(p)], dim=-1)
|
| 33 |
+
return self.net(h)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class T2EnumSpecialist:
|
| 37 |
+
def __init__(self, weights_path, device):
|
| 38 |
+
blob = torch.load(weights_path, map_location=device, weights_only=True)
|
| 39 |
+
self.model = T2Net(**blob["config"]).to(device)
|
| 40 |
+
self.model.load_state_dict(blob["state_dict"])
|
| 41 |
+
self.model.eval()
|
| 42 |
+
self.device = device
|
| 43 |
+
|
| 44 |
+
@torch.no_grad()
|
| 45 |
+
def predict_batch(self, batch) -> list[list[int]]:
|
| 46 |
+
ra = torch.tensor([r_a for r_a, _, _ in batch], dtype=torch.long, device=self.device)
|
| 47 |
+
rb = torch.tensor([r_b for _, r_b, _ in batch], dtype=torch.long, device=self.device)
|
| 48 |
+
p = torch.tensor([p_enc["p"] for _, _, p_enc in batch], dtype=torch.long, device=self.device)
|
| 49 |
+
preds = self.model(ra, rb, p).argmax(-1).tolist()
|
| 50 |
+
return [[int(v)] for v in preds]
|
weights/mont_cells.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a9a0e9ff04b2e1610fdd8779e275a3c1bad73ceef53062d831bc0e06d067786e
|
| 3 |
+
size 2553417
|
weights/t2_enum.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5ed877c05fd58e37b362645667876cc40c9afac0572e875d651c6c022832b576
|
| 3 |
+
size 42758786
|