mikelou commited on
Commit
efe8193
·
verified ·
1 Parent(s): 68399d0

Add faster compact NeuralHorner v8 prototype

Browse files

49.8% smaller checkpoint; two-pass Horner schedule; reusable static feature channels. Public Tiers 1-7: 700/700 locally. Tier 6 MPS: 19.07s vs 27.02s baseline. Full CUDA Tier 1-10 validation remains required.

Files changed (6) hide show
  1. LICENSE +21 -0
  2. README.md +52 -0
  3. RESULTS.md +16 -0
  4. manifest.json +7 -0
  5. model.py +183 -0
  6. weights.pt +3 -0
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Robert Sneiderman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -1,3 +1,55 @@
1
  ---
2
  license: mit
 
 
 
 
 
 
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  license: mit
3
+ library_name: pytorch
4
+ tags:
5
+ - modular-arithmetic
6
+ - neural-arithmetic
7
+ - bit-serial
8
+ - gru
9
  ---
10
+
11
+ # NeuralHorner v8 compact inference experiment
12
+
13
+ This local derivative tests storage and inference optimizations against the
14
+ published MIT-licensed `TrickyRex/bitserial-modmul-v8` checkpoint. It is not a
15
+ newly trained model. Original model and weights by Robert Sneiderman:
16
+ <https://huggingface.co/TrickyRex/bitserial-modmul-v8>.
17
+
18
+ Changes:
19
+
20
+ - checkpoint tensors stored as bfloat16;
21
+ - direct `logit > 0` decisions instead of `sigmoid(logit) > 0.5`;
22
+ - one operand is reduced and the other is streamed directly through the
23
+ learned multiplication transition, removing one modulus-width pass;
24
+ - static multiplicand/modulus feature channels are allocated once per scan
25
+ instead of rebuilt at every recurrent step.
26
+
27
+ The last change is mathematically equivalent for an exact transition, but the
28
+ learned cell is not proven exact. Benchmark equivalence therefore must be
29
+ measured before this is treated as an accuracy-preserving release.
30
+
31
+ ## Local validation
32
+
33
+ Hardware: Apple GPU through PyTorch MPS. Dataset: the official 100-case public
34
+ benchmark for each tier.
35
+
36
+ - Tiers 1-7: 700/700 exact.
37
+ - Tier 6 runtime: 19.07 seconds versus 27.02 seconds for the published wrapper
38
+ (29.4% lower wall-clock in this comparison). The earlier short-schedule
39
+ version took 20.45-21.31 seconds before static-channel reuse.
40
+ - Tier 6 outputs: 100/100 byte-for-byte identical to the published wrapper.
41
+ - The bfloat16 checkpoint with the original three-pass schedule also produced
42
+ 100/100 outputs identical to the fp32 checkpoint on Tier 6.
43
+ - Official static analysis: clean.
44
+
45
+ The checkpoint is 948,196 bytes versus 1,887,610 bytes (49.8% smaller). The
46
+ shorter schedule removes exactly one `Leff` recurrent pass: 20% of recurrent
47
+ step calls on public Tiers 3 and 5-10, 14.3% on Tier 4, 25% on Tier 2, and
48
+ 33.3% on Tier 1.
49
+
50
+ ## Release gate
51
+
52
+ Do not claim unchanged Tier 1-10 accuracy yet. The current machine is too slow
53
+ for a practical full-width Tier 8-10 MPS run. Before release, run the official
54
+ 1,100-case scorer on CUDA for the three published seeds and require 100% on
55
+ every scored tier, then repeat the held-out 768-case adversarial battery.
RESULTS.md ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Optimization results
2
+
3
+ | Variant | Checkpoint | Schedule | Tier 6 accuracy | Tier 6 time (MPS) |
4
+ |---|---:|---|---:|---:|
5
+ | Published v8 | 1,887,610 B | reduce a + reduce b + multiply | 100/100 | 27.02 s |
6
+ | Compact weights | 948,196 B | reduce a + reduce b + multiply | 100/100, identical outputs | 34.59 s* |
7
+ | Compact + short schedule | 948,196 B | reduce a + multiply by streaming b | 100/100, identical outputs | 20.45-21.31 s |
8
+ | Compact + short schedule + static-channel reuse | 948,196 B | reduce a + multiply by streaming b | 100/100 | 19.07 s |
9
+
10
+ `*` This single MPS timing is noisy and does not imply bfloat16 storage makes
11
+ inference slower. The compact tensors are loaded back into fp32 parameters;
12
+ storage precision affects artifact size, not the execution dtype.
13
+
14
+ The short-schedule variant also scored 100/100 on each public Tier 1 through 7
15
+ (700/700 total) and passed the official static checker. Full Tier 8-10 and
16
+ held-out adversarial validation remain release blockers.
manifest.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "entry_class": "model.BitSerialReducer",
3
+ "output_base": 2,
4
+ "framework": "pytorch",
5
+ "model_description": "Compute-optimized NeuralHorner v8 inference wrapper using the original ~471K-parameter bidirectional two-layer GRU transition. Stores weights in bfloat16, thresholds logits directly, reuses static feature channels, and removes one redundant modular-reduction pass by streaming one original operand directly through the learned Horner multiply transition.",
6
+ "training_description": "Uses the published TrickyRex/bitserial-modmul-v8 weights (MIT), warm-started and fine-tuned by its author on one-step modular transitions. This derivative changes checkpoint precision and the mathematically equivalent inference schedule; it does not retrain the learned transition."
7
+ }
model.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Smaller, lower-compute inference wrapper for NeuralHorner v8.
2
+
3
+ The learned transition is unchanged. Compared with the published wrapper:
4
+
5
+ * checkpoint tensors may be stored in bfloat16 and are restored to float32;
6
+ * logits are thresholded at zero (exactly equivalent to sigmoid(logit) > 0.5);
7
+ * only one operand is reduced before multiplication. The other operand is
8
+ streamed directly through the same Horner transition, eliminating a full
9
+ modulus-width recurrent pass.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from pathlib import Path
15
+
16
+ import torch
17
+ from torch import nn
18
+
19
+ from modchallenge.interface.base_model import ModularMultiplicationModel
20
+
21
+ _MASK32 = (1 << 32) - 1
22
+
23
+
24
+ def _to_bits_small(vals: torch.Tensor, width: int) -> torch.Tensor:
25
+ shifts = torch.arange(width - 1, -1, -1, device=vals.device)
26
+ return (vals[:, None] >> shifts[None, :]) & 1
27
+
28
+
29
+ def to_bits_limbs(ints, dev, width: int) -> torch.Tensor:
30
+ nl = (width + 31) // 32
31
+ cols = []
32
+ for k in range(nl - 1, -1, -1):
33
+ limb = torch.tensor(
34
+ [(v >> (32 * k)) & _MASK32 for v in ints],
35
+ dtype=torch.int64,
36
+ device=dev,
37
+ )
38
+ cols.append(_to_bits_small(limb, 32))
39
+ bits = torch.cat(cols, dim=1)
40
+ return bits[:, nl * 32 - width:] if width < nl * 32 else bits
41
+
42
+
43
+ class Cell(nn.Module):
44
+ def __init__(self, dmodel: int = 96, hidden: int = 128):
45
+ super().__init__()
46
+ self.in_proj = nn.Linear(3, dmodel)
47
+ self.d_emb = nn.Embedding(2, dmodel)
48
+ self.gru = nn.GRU(
49
+ dmodel,
50
+ hidden,
51
+ num_layers=2,
52
+ batch_first=True,
53
+ bidirectional=True,
54
+ )
55
+ self.head = nn.Linear(2 * hidden, 1)
56
+
57
+ def forward(self, feat, d):
58
+ x = self.in_proj(feat) + self.d_emb(d)[:, None, :]
59
+ h, _ = self.gru(x)
60
+ return self.head(h).squeeze(-1)
61
+
62
+
63
+ def _bits_of(n: int) -> list[int]:
64
+ if n <= 0:
65
+ return [0]
66
+ out: list[int] = []
67
+ while n > 0:
68
+ out.append(n & 1)
69
+ n >>= 1
70
+ out.reverse()
71
+ return out
72
+
73
+
74
+ class BitSerialReducer(ModularMultiplicationModel):
75
+ def __init__(self) -> None:
76
+ self.model: Cell | None = None
77
+ self.device: torch.device | None = None
78
+ self.L = 32
79
+ self._Leff = 32
80
+
81
+ def load(self, model_dir: str) -> None:
82
+ if torch.cuda.is_available():
83
+ self.device = torch.device("cuda")
84
+ elif torch.backends.mps.is_available():
85
+ self.device = torch.device("mps")
86
+ else:
87
+ self.device = torch.device("cpu")
88
+ ckpt = torch.load(
89
+ Path(model_dir) / "weights.pt",
90
+ map_location="cpu",
91
+ weights_only=True,
92
+ )
93
+ self.L = int(ckpt.get("L", 32))
94
+ self.model = Cell(**ckpt.get("config", {}))
95
+ # load_state_dict casts compact bf16 checkpoint tensors back to fp32.
96
+ self.model.load_state_dict(ckpt["state_dict"])
97
+ self.model.to(self.device)
98
+ self.model.eval()
99
+ self.model.gru.flatten_parameters()
100
+
101
+ def preprocess_a(self, a):
102
+ return _bits_of(int(a))
103
+
104
+ def preprocess_b(self, b):
105
+ return _bits_of(int(b))
106
+
107
+ def preprocess_p(self, p):
108
+ return int(p)
109
+
110
+ @torch.inference_mode()
111
+ def predict_digits(self, a_enc, b_enc, p_enc):
112
+ return self.predict_digits_batch([(a_enc, b_enc, p_enc)])[0]
113
+
114
+ @torch.inference_mode()
115
+ def predict_digits_batch(self, inputs):
116
+ L = self.L
117
+ max_op = 4 * L
118
+ out: list[list[int]] = [[0] for _ in inputs]
119
+ idx, a_lists, b_lists, p_vals = [], [], [], []
120
+ for i, (a_enc, b_enc, p_enc) in enumerate(inputs):
121
+ p = int(p_enc)
122
+ a_bits = list(a_enc)
123
+ b_bits = list(b_enc)
124
+ if p < 2 or p >= (1 << L) or len(a_bits) > max_op or len(b_bits) > max_op:
125
+ continue
126
+ idx.append(i)
127
+ a_lists.append(a_bits)
128
+ b_lists.append(b_bits)
129
+ p_vals.append(p)
130
+ if not idx:
131
+ return out
132
+
133
+ dev = self.device
134
+ maxp = max(int(p).bit_length() for p in p_vals)
135
+ self._Leff = min(self.L, max(32, ((maxp + 31) // 32) * 32))
136
+ p_bits = to_bits_limbs(p_vals, dev, self._Leff).float()
137
+
138
+ # (a*b) mod p = ((a mod p)*b) mod p. Streaming the original b bits
139
+ # through the learned Horner cell avoids first reducing b and then
140
+ # scanning its L-bit residue a second time.
141
+ ra = self._reduce(a_lists, p_bits, dev)
142
+ prod = self._scan(b_lists, ra, p_bits, dev)
143
+ prod_list = prod.long().tolist()
144
+ for j, i in enumerate(idx):
145
+ out[i] = [int(x) for x in prod_list[j]]
146
+ return out
147
+
148
+ def max_batch_size(self) -> int:
149
+ return 256
150
+
151
+ def _step(self, s_bits, feat, d):
152
+ # The multiplicand and modulus channels stay constant for an entire
153
+ # scan. Reuse their preallocated feature tensor instead of rebuilding
154
+ # and copying all three channels at every recurrent step.
155
+ feat[:, :, 0].copy_(s_bits)
156
+ if self.device is not None and self.device.type == "cuda":
157
+ with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
158
+ logits = self.model(feat, d)
159
+ # Comparing a bf16 value with zero has the same sign decision as
160
+ # first widening it to fp32, without allocating the fp32 logits.
161
+ return (logits > 0).float()
162
+ return (self.model(feat, d) > 0).float()
163
+
164
+ def _scan(self, bit_lists, x_bits, p_bits, dev):
165
+ n = len(bit_lists)
166
+ width = max(len(bits) for bits in bit_lists)
167
+ padded = torch.zeros((n, width), dtype=torch.long, device=dev)
168
+ for row, bits in enumerate(bit_lists):
169
+ if bits:
170
+ padded[row, width - len(bits):] = torch.tensor(
171
+ bits, dtype=torch.long, device=dev
172
+ )
173
+ state = torch.zeros((n, self._Leff), device=dev)
174
+ feat = torch.empty((n, self._Leff, 3), device=dev)
175
+ feat[:, :, 1].copy_(x_bits)
176
+ feat[:, :, 2].copy_(p_bits)
177
+ for pos in range(width):
178
+ state = self._step(state, feat, padded[:, pos])
179
+ return state
180
+
181
+ def _reduce(self, bit_lists, p_bits, dev):
182
+ ones = to_bits_limbs([1] * len(bit_lists), dev, self._Leff).float()
183
+ return self._scan(bit_lists, ones, p_bits, dev)
weights.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e09c1037ea89f8817db0a488e3179512e829175fd23d313e30b69d422baac16d
3
+ size 948196