File size: 6,204 Bytes
14bef4a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | """Generalized long-division reduction CoT — base-B limbs, estimated quotient,
optional scratchpad. Scales divcot_encoding.py (validated at tier 2, base 10)
to higher tiers.
Three levers over the validated v1:
1. **Base-B limbs** (B=100 halves W): each limb is one byte (limbs.py), so
tier 5's W=20 decimal becomes 10 limbs, tier 7's W=78 becomes 39.
2. **Estimated-quotient install** (`qhat`): the classic scaling trick. The
one-shot quotient digit qd = floor((r*B+d)/p) needs a W-limb comparison —
fine at W=3, strained at W>=5. Humans estimate qd from the LEADING limbs
of numerator and divisor (a constant-size table independent of W), then
correct by ±1. We install that estimate at an early block so the circuit
the model learns is estimate-then-correct, not a W-limb divide.
3. **Scratchpad mode** (`scratch=True`): for large W the model can't hold the
running remainder internally — EMIT it (LSB-first, so the subtraction
borrow chain is local). Cost: O(W^2) tokens vs O(W) compact; use compact
while it trains, scratch when it stops.
Surface (compact): "N" N(2W,MSB) "M" p(W,MSB) "=" Q(2W) "R" ans(W,LSB) "\n"
Surface (scratch): ... "=" [qd r(W,LSB)]*2W "R" ans(W,LSB) "\n"
Install targets (NTP-aligned: label at position t supervises the residual
predicting byte t+1):
rem{j} compact only — MSB limb j of the running remainder at each
quotient-emission position (the division carry/state)
qhat leading-limbs quotient estimate at each quotient position
ans limb k of N mod p at each answer position
"""
from __future__ import annotations
from limbs import limb_char, limb_str, parse_limbs, to_limbs
NMARK, DIVMARK, EQ, REVMARK, NL = "N", "M", "=", "R", "\n"
def long_division(N: int, p: int, W: int, base: int):
"""(quotient_limbs[2W] MSB-first, remainders[2W], answer)."""
q_limbs, rems, r = [], [], 0
for d in to_limbs(N, 2 * W, base, msb_first=True):
r = r * base + d
qd = r // p # single limb 0..base-1 (r < base*p)
r -= qd * p
q_limbs.append(qd)
rems.append(r)
return q_limbs, rems, r # r == N % p
def qhat_estimate(r_prev: int, d: int, p: int, base: int) -> int:
"""Leading-limbs estimate of floor((r*B+d)/p): numerator's top two limbs
over divisor's top limb — constant-size lookup regardless of W."""
num = r_prev * base + d
if num < p:
return 0
nw = 1
while base ** nw <= num:
nw += 1
pw = 1
while base ** pw <= p:
pw += 1
n_top = num // base ** max(0, nw - 2) # top 2 limbs of numerator
p_top = p // base ** (pw - 1) # top 1 limb of divisor
shift = (nw - 2) - (pw - 1)
est = (n_top // p_top) * base ** shift if shift >= 0 else n_top // (p_top * base ** -shift)
return min(base - 1, max(0, est))
def prompt_str(N: int, p: int, W: int, base: int) -> str:
return NMARK + limb_str(N, 2 * W, base, msb_first=True) \
+ DIVMARK + limb_str(p, W, base, msb_first=True) + EQ
def gen_len(W: int, scratch: bool = False) -> int:
steps = 2 * W * (1 + W) if scratch else 2 * W
return steps + 1 + W
def build_example(N: int, p: int, W: int, base: int, scratch: bool = False):
q_limbs, rems, answer = long_division(N, p, W, base)
n_msb = to_limbs(N, 2 * W, base, msb_first=True)
prompt = prompt_str(N, p, W, base)
parts, ann_q = [], [] # ann_q: (char_index, var, val)
pos = len(prompt)
r_prev = 0
for i, (qd, r) in enumerate(zip(q_limbs, rems)):
ann_q.append((pos, "qhat", qhat_estimate(r_prev, n_msb[i], p, base)))
if not scratch: # compact: remainder is internal state
rstr = to_limbs(r, W, base, msb_first=True)
for j in range(W):
ann_q.append((pos, f"rem{j}", rstr[j]))
parts.append(limb_char(qd))
pos += 1
if scratch: # emit remainder LSB-first (local borrow)
parts.append(limb_str(r, W, base))
pos += W
r_prev = r
ans_limbs = to_limbs(answer, W, base) # LSB-first
parts.append(REVMARK + limb_str(answer, W, base) + NL)
text = prompt + "".join(parts)
ann = [dict() for _ in range(len(text))]
for idx, var, val in ann_q:
ann[idx - 1][var] = int(val) # NTP alignment
ans_base = pos + 1 # after 'R'
for k in range(W):
ann[ans_base + k - 1]["ans"] = ans_limbs[k]
return text, ann
def var_specs(W: int, base: int, scratch: bool = False):
"""(name, n_classes) install vars; assign blocks in the trainer."""
specs = [("ans", base), ("qhat", base)]
if not scratch:
specs += [(f"rem{j}", base) for j in range(W)]
return specs
def decode_answer(gen_chars: str, W: int, base: int, scratch: bool = False) -> int:
off = (2 * W * (1 + W) if scratch else 2 * W) + 1 # skip quotient(+rems) and 'R'
return parse_limbs(gen_chars[off:off + W], base)
if __name__ == "__main__":
import random
rng = random.Random(1)
for base in (10, 100):
for scratch in (False, True):
for _ in range(4000):
W = rng.randint(1, 8)
p = rng.randrange(max(2, base ** (W - 1)), base ** W)
N = rng.randrange(p * p) if p > 1 else 0
text, ann = build_example(N, p, W, base, scratch)
plen = len(prompt_str(N, p, W, base))
assert len(text) == plen + gen_len(W, scratch) + 1, (len(text), plen, gen_len(W, scratch)) # +1: NL
assert decode_answer(text[plen:], W, base, scratch) == N % p
assert len(ann) == len(text)
print(f"divcot2 base={base} scratch={scratch}: 4000/4000 decode OK")
# tier-relevant lengths
print("\ntokens/example (prompt+gen):")
for tier, Wd in [(3, 5), (4, 10), (5, 20), (6, 39), (7, 78), (8, 155)]:
for base, W in ((10, Wd), (100, (Wd + 1) // 2)):
plen = 3 * W + 3
print(f" tier {tier} base {base:>3} W={W:>3} compact {plen + gen_len(W)}"
f" scratch {plen + gen_len(W, True)}")
|