Download encoding.py from alstrup/modmul-challenge: direct link, hf CLI and curl.
- Browser
- Download file 3.89 kB
-
https://huggingface.co/alstrup/modmul-challenge/resolve/main/encoding.py
- Command line
-
hf download hf://alstrup/modmul-challenge/encoding.py
-
curl -L -o encoding.py https://huggingface.co/alstrup/modmul-challenge/resolve/main/encoding.py
3.89 kB
| """Shared sequence encoding for the modmul BP-install model. | |
| The model learns the field-multiplication map (p, x, y) -> (x*y) mod p with | |
| x, y in [0, p). At inference the legal two-operand reduction a%p, b%p produces | |
| x, y (the same step the baselines use); training samples x, y in [0, p) directly, | |
| which matches the (a mod p) distribution for p << operand range. | |
| Fixed-width, base-10, reverse-LSB answer (the proven arithmetic recipe). The | |
| prime p is an explicit in-sequence conditioning field so one model generalises | |
| across primes: | |
| p007*012,003=R120\n # W=3 example: p=7, x=12%7=5? no -- x,y already <p | |
| # e.g. p=7,x=5,y=4 -> 20%7=6 -> "p7*5,4=R6\n" (W=1) | |
| Layout (width W = max decimal digits of any prime in scope): | |
| "p" P(W) "*" X(W) "," Y(W) "=R" REV_ANS(W) "\n" | |
| zero-padded fields; REV_ANS is the W-digit answer reversed (least-significant | |
| digit first), so digit k is emitted at a fixed position and the carry flows | |
| left-to-right the way reverse-LSB addition does. | |
| """ | |
| from __future__ import annotations | |
| MUL, SEP, EQ, REVMARK, NL = "*", ",", "=", "R", "\n" | |
| NEWLINE_ID = ord("\n") | |
| def width_for_primes(primes) -> int: | |
| """Decimal digits needed to hold the largest residue (p-1).""" | |
| return max(len(str(p - 1)) for p in primes) | |
| def prompt_str(p: int, x: int, y: int, W: int) -> str: | |
| return f"p{p:0{W}d}*{x:0{W}d},{y:0{W}d}={REVMARK}" | |
| def build_example(p: int, x: int, y: int, W: int): | |
| """Return (text, ann) where ann[t] holds the install label for the residual | |
| at byte position t (which predicts byte t+1), NTP-aligned.""" | |
| ans = (x * y) % p | |
| rev_ans = f"{ans:0{W}d}"[::-1] # LSB-first, width W | |
| prompt = prompt_str(p, x, y, W) | |
| text = prompt + rev_ans + NL | |
| ann = [dict() for _ in range(len(text))] | |
| base = len(prompt) # first answer char index | |
| for k in range(W): # answer digit k (LSB-first) | |
| pos = base + k | |
| ann[pos - 1]["ans"] = int(rev_ans[k]) # residual at pos-1 predicts it | |
| return text, ann | |
| def answer_len(W: int) -> int: | |
| return W | |
| # --- Scratchpad ("chain-of-thought") inference variant ------------------- | |
| # The model GENERATES the product as reverse-LSB digits before the answer: | |
| # "p" P(W) "*" X(W) "," Y(W) "=" REV_PROD(2W) "R" REV_ANS(W) "\n" | |
| # At inference we feed up to '=' and greedily decode the full scratchpad; the | |
| # answer is the last W generated digits. Python never computes x*y -- the model | |
| # emits the product digits (a learned circuit). Only a%p, b%p is done in code. | |
| def prompt_str_sp(p: int, x: int, y: int, W: int) -> str: | |
| return f"p{p:0{W}d}*{x:0{W}d},{y:0{W}d}{EQ}" | |
| def scratchpad_len(W: int) -> int: | |
| return 2 * W + 1 + W | |
| def decode_answer_sp(gen_chars: str, W: int) -> int: | |
| """Generated suffix is 2W product digits, 'R', then W answer digits.""" | |
| return decode_answer(gen_chars[2 * W + 1 : 2 * W + 1 + W], W) | |
| # --- Schoolbook ("partial products") inference variant ------------------- | |
| # Generated suffix: W partial products (W+1 wide), 'P', 2W product digits, | |
| # 'R', W answer digits. The model emits the partial products and sums them | |
| # (learned circuit); the answer is the final W digits. | |
| def school_gen_len(W: int) -> int: | |
| return W * (W + 1) + 1 + 2 * W + 1 + W | |
| def decode_answer_school(gen_chars: str, W: int) -> int: | |
| start = W * (W + 1) + 1 + 2 * W + 1 | |
| return decode_answer(gen_chars[start:start + W], W) | |
| def decode_answer(gen_chars: str, W: int) -> int: | |
| """Greedy-generated W chars are the reverse-LSB answer digits. Non-digits | |
| (untrained junk) count as 0. Returns the integer answer.""" | |
| val = 0 | |
| for k in range(min(W, len(gen_chars))): | |
| c = gen_chars[k] | |
| d = int(c) if "0" <= c <= "9" else 0 # ASCII only ('³'.isdigit() is True but int() fails) | |
| val += d * (10 ** k) | |
| return val | |