alstrup commited on
Commit
a99bce6
·
verified ·
1 Parent(s): 5052e8b

tier-4 SRT member: composed_encoding.py

Browse files
Files changed (1) hide show
  1. composed_encoding.py +229 -30
composed_encoding.py CHANGED
@@ -50,7 +50,9 @@ def prompt_str(x: int, y: int, p: int, W: int, base: int, subpad: bool = False)
50
 
51
 
52
  def gen_len(W: int, scratch: bool = False, cursor: bool = False,
53
- subpad: bool = False) -> int:
 
 
54
  """Tokens generated after '=': W acc blocks, N restage, quotient, answer.
55
  scratch=True: each quotient limb is followed by the running remainder
56
  (W limbs, LSB-first) — externalizes the division state, which the model
@@ -66,16 +68,94 @@ def gen_len(W: int, scratch: bool = False, cursor: bool = False,
66
  per-step diagnostic showed failing (steps 0-4 = copies, perfect; real
67
  division steps 5+ at 0.04-0.15 even teacher-forced). Tier-2's compact
68
  success could memorize qd*p over 48 primes; tier 3's 400 primes need the
69
- generic circuit, so generate it like the (perfectly learned) acc rows."""
70
- per_step = (1 if cursor else 0) + 1 + ((W + 1) if subpad else 0) + (W if scratch else 0)
71
- return W * (1 + 2 * W) + (1 + 2 * W) + 2 * W * per_step + 1 + W
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
 
74
  def build_example(x: int, y: int, p: int, W: int, base: int, scratch: bool = False,
75
- cursor: bool = False, subpad: bool = False):
 
 
76
  """Return (text, ann). x,y < p < base**W."""
 
 
 
 
77
  assert not cursor or scratch, "cursor requires scratch"
78
  assert not subpad or cursor, "subpad requires cursor"
 
 
 
 
79
  N = x * y
80
  y_limbs = to_limbs(y, W, base)
81
  x_limbs = to_limbs(x, W, base)
@@ -98,17 +178,103 @@ def build_example(x: int, y: int, p: int, W: int, base: int, scratch: bool = Fal
98
  pos += 2 * W
99
  assert acc == N
100
 
101
- parts.append(NMARK + limb_str(N, 2 * W, base, msb_first=True)) # restage MSB
102
- pos += 1 + 2 * W
103
-
104
- q_limbs, rems, answer = long_division(N, p, W, base)
105
  n_msb = to_limbs(N, 2 * W, base, msb_first=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  r_prev = 0
107
- for i, (qd, r) in enumerate(zip(q_limbs, rems)):
 
 
 
 
 
 
 
108
  if cursor: # copy the consumed dividend limb
109
  parts.append(limb_char(n_msb[i]))
110
  pos += 1
111
- ann_g.append((pos, "qhat", qhat_estimate(r_prev, n_msb[i], p, base)))
 
 
 
 
 
 
 
 
 
112
  if not scratch: # compact: remainder is internal state
113
  rstr = to_limbs(r, W, base, msb_first=True)
114
  for j in range(W):
@@ -125,15 +291,23 @@ def build_example(x: int, y: int, p: int, W: int, base: int, scratch: bool = Fal
125
  parts.append(limb_str(qdp, W + 1, base))
126
  pos += W + 1
127
  if scratch: # emit remainder LSB-first (local borrow)
128
- if subpad: # borrow chain of num - qd*p
129
- num_l = [n_msb[i]] + to_limbs(r_prev, W, base)
130
- qdp_l = to_limbs(qd * p, W + 1, base)
 
131
  b = 0
132
  for k in range(W):
133
- ann_g.append((pos + k, "sborrow", b))
134
  b = 1 if num_l[k] - qdp_l[k] - b < 0 else 0
135
- parts.append(limb_str(r, W, base))
136
- pos += W
 
 
 
 
 
 
 
137
  r_prev = r
138
  parts.append(REVMARK)
139
  pos += 1
@@ -145,8 +319,10 @@ def build_example(x: int, y: int, p: int, W: int, base: int, scratch: bool = Fal
145
 
146
  prompt = prompt_str(x, y, p, W, base, subpad)
147
  text = prompt + "".join(parts)
148
- assert len(text) == len(prompt) + gen_len(W, scratch, cursor, subpad) + 1, \
149
- (len(text), len(prompt), gen_len(W, scratch, cursor, subpad))
 
 
150
  ann = [dict() for _ in range(len(text))]
151
  base_i = len(prompt)
152
  for gi, var, val in ann_g:
@@ -154,8 +330,10 @@ def build_example(x: int, y: int, p: int, W: int, base: int, scratch: bool = Fal
154
  return text, ann
155
 
156
 
157
- def var_specs(W: int, base: int, scratch: bool = False, subpad: bool = False):
158
- specs = [("ans", base), ("qhat", base), ("acarry", base)]
 
 
159
  if subpad: # multiply-carry + subtract-borrow chains
160
  specs += [("spcarry", base), ("sborrow", 2)]
161
  if not scratch: # scratch emits remainders: no probe
@@ -164,8 +342,13 @@ def var_specs(W: int, base: int, scratch: bool = False, subpad: bool = False):
164
 
165
 
166
  def decode_answer(gen_chars: str, W: int, base: int, scratch: bool = False,
167
- cursor: bool = False, subpad: bool = False) -> int:
168
- off = gen_len(W, scratch, cursor, subpad) - W # answer = last W generated limbs
 
 
 
 
 
169
  return parse_limbs(gen_chars[off:off + W], base)
170
 
171
 
@@ -173,19 +356,35 @@ if __name__ == "__main__":
173
  import random
174
  rng = random.Random(3)
175
  for base in (10, 100):
176
- for scratch, cursor, subpad in ((False, False, False), (True, False, False),
177
- (True, True, False), (True, True, True)):
 
 
 
 
 
 
 
 
 
 
178
  for _ in range(4000):
179
- W = rng.randint(1, 6)
180
  p = rng.randrange(max(2, base ** (W - 1)), base ** W)
181
  x, y = rng.randrange(p), rng.randrange(p)
182
- text, ann = build_example(x, y, p, W, base, scratch, cursor, subpad)
 
 
183
  plen = len(prompt_str(x, y, p, W, base, subpad))
184
- assert decode_answer(text[plen:], W, base, scratch, cursor, subpad) \
185
- == (x * y) % p, (x, y, p, W, base, scratch, cursor, subpad)
 
 
 
186
  assert len(ann) == len(text)
187
  print(f"composed base={base} scratch={scratch} cursor={cursor} "
188
- f"subpad={subpad}: 4000/4000 decode OK")
 
189
  print("\ntokens/example (prompt+gen+NL):")
190
  for tier, Wd in [(3, 5), (4, 10), (5, 20), (6, 39), (7, 78)]:
191
  for base, W in ((10, Wd), (100, (Wd + 1) // 2)):
 
50
 
51
 
52
  def gen_len(W: int, scratch: bool = False, cursor: bool = False,
53
+ subpad: bool = False, stepidx: bool = False,
54
+ skiptriv: bool = False, subnum: bool = False,
55
+ bemit: bool = False, srt: bool = False) -> int:
56
  """Tokens generated after '=': W acc blocks, N restage, quotient, answer.
57
  scratch=True: each quotient limb is followed by the running remainder
58
  (W limbs, LSB-first) — externalizes the division state, which the model
 
68
  per-step diagnostic showed failing (steps 0-4 = copies, perfect; real
69
  division steps 5+ at 0.04-0.15 even teacher-forced). Tier-2's compact
70
  success could memorize qd*p over 48 primes; tier 3's 400 primes need the
71
+ generic circuit, so generate it like the (perfectly learned) acc rows.
72
+ stepidx=True (requires cursor): each step opens with its 2-digit ASCII
73
+ index, and the restage pairs every dividend limb with its index a
74
+ content-addressable name per step. Tier-4 diagnostic (W=10, 20 steps):
75
+ steps 0-9 at ~1.00, cliff to ~0.45 at 10+; rem fails worst (0.36 late)
76
+ despite all-local operands, so the wall is step MISBINDING under
77
+ relative-position (ALiBi) attention — the number of identical-looking
78
+ distractor blocks grows with step index. Indexed steps turn 'previous
79
+ step's remainder' and 'my dividend limb' into content lookups.
80
+ skiptriv=True (requires stepidx): x,y < p guarantees Q = N//p < p, so the
81
+ first W division steps ALWAYS emit qd=0 — provably trivial filler that
82
+ doubles the distractor count, dilutes gradient (250 of 828 tokens at
83
+ W=10), and pushes real steps ~250 tokens further from the prompt's p
84
+ field. Skip them: division = W real steps keeping their TRUE indices
85
+ (so restage lookups still match), starting from r_prev = N's top half.
86
+ Also re-emits p LSB-first ('m' + W limbs) right after the restage, so
87
+ qd*p rows read a local p copy instead of a ~700-token fetch (tier-4
88
+ stepidx diagnostic: qdp 0.78-0.84 was the residual fetch weakness).
89
+ subnum=True (requires subpad): each step copies its numerator
90
+ num = r_prev*base + cursor limb (W+1 limbs, LSB) between cursor and qd,
91
+ so qd / qd*p / rem are all WITHIN-step local arithmetic. skiptriv
92
+ diagnostic: qdp hit 0.9949 once p became a local read while rem
93
+ plateaued at 0.43 fetching the previous step's rem block -- every pure
94
+ copy in this stack reaches ~1.00, every fetch+arithmetic fusion stalls;
95
+ decouple them (the subpad lesson, applied to the subtraction).
96
+ bemit=True (requires subnum): the remainder block becomes (borrow, limb)
97
+ pairs -- the borrow bit is EMITTED as an ASCII 0/1 token before each rem
98
+ limb, so b_k sits in context when rem[k] is predicted and b_{k+1} is a
99
+ local function of (num[k], qdp[k], b_k), all adjacent. v4 diagnostic:
100
+ rem errors were 58% unstructured noise, 0.54 even at b=0 -- the
101
+ install-only borrow supervision never built a circuit; emit the chain
102
+ (the scratch lesson, applied to the borrow state)."""
103
+ restage = 1 + (3 if stepidx else 1) * 2 * W
104
+ pcopy = (1 + W) if skiptriv else 0
105
+ if srt:
106
+ # stepi + cursor + num complement (W+2) + qd + |qd|*p (W+1) + pairs
107
+ per_step = 2 + 1 + (W + 2) + 1 + (W + 1) + 2 * (W + 2)
108
+ final = 1 + 2 * W # sign + correction pairs
109
+ return (W * (1 + 2 * W) + restage + pcopy + W * per_step
110
+ + final + 1 + W)
111
+ per_step = ((2 if stepidx else 0) + (1 if cursor else 0)
112
+ + ((W + 1) if subnum else 0) + 1
113
+ + ((W + 1) if subpad else 0)
114
+ + ((2 * W) if bemit else (W if scratch else 0)))
115
+ n_steps = W if skiptriv else 2 * W
116
+ return W * (1 + 2 * W) + restage + pcopy + n_steps * per_step + 1 + W
117
+
118
+
119
+ def _srt_qd(num: int, p: int, base: int) -> int:
120
+ """Signed SRT digit from |num|'s top-4 / p's top-3 limbs (exact integer
121
+ rounding). Guarantees |num - qd*p| < p given |num| < base*p (verified:
122
+ 0 violations / 500k tier-4 selections, worst |s|/p = 0.54)."""
123
+ if num == 0:
124
+ return 0
125
+ sgn = 1 if num > 0 else -1
126
+ an = abs(num)
127
+ nw = 1
128
+ while base ** nw <= an:
129
+ nw += 1
130
+ pw = 1
131
+ while base ** pw <= p:
132
+ pw += 1
133
+ n4 = an // base ** max(0, nw - 4)
134
+ p3 = p // base ** max(0, pw - 3)
135
+ sh = max(0, nw - 4) - max(0, pw - 3)
136
+ if sh >= 0:
137
+ a, b = n4 * base ** sh, p3
138
+ else:
139
+ a, b = n4, p3 * base ** -sh
140
+ q = (2 * a + b) // (2 * b) # round(a/b), exact
141
+ return sgn * min(base - 1, q)
142
 
143
 
144
  def build_example(x: int, y: int, p: int, W: int, base: int, scratch: bool = False,
145
+ cursor: bool = False, subpad: bool = False, stepidx: bool = False,
146
+ skiptriv: bool = False, subnum: bool = False,
147
+ bemit: bool = False, srt: bool = False):
148
  """Return (text, ann). x,y < p < base**W."""
149
+ assert not subnum or subpad, "subnum requires subpad"
150
+ assert not bemit or subnum, "bemit requires subnum"
151
+ assert not srt or (skiptriv and bemit), "srt requires skiptriv+bemit"
152
+ assert not srt or base == 10, "srt: signed-digit tokens are base-10 only"
153
  assert not cursor or scratch, "cursor requires scratch"
154
  assert not subpad or cursor, "subpad requires cursor"
155
+ assert not stepidx or cursor, "stepidx requires cursor"
156
+ assert not skiptriv or stepidx, "skiptriv requires stepidx"
157
+ assert not stepidx or 2 * W <= 100, "stepidx: 2-digit indices cap at 100 steps"
158
+ assert not skiptriv or (x < p and y < p), "skiptriv: Q < p needs x,y < p"
159
  N = x * y
160
  y_limbs = to_limbs(y, W, base)
161
  x_limbs = to_limbs(x, W, base)
 
178
  pos += 2 * W
179
  assert acc == N
180
 
 
 
 
 
181
  n_msb = to_limbs(N, 2 * W, base, msb_first=True)
182
+ if stepidx: # restage as (index, limb) pairs
183
+ parts.append(NMARK + "".join(f"{i:02d}" + limb_char(n_msb[i])
184
+ for i in range(2 * W)))
185
+ pos += 1 + 3 * 2 * W
186
+ else:
187
+ parts.append(NMARK + limb_str(N, 2 * W, base, msb_first=True)) # restage MSB
188
+ pos += 1 + 2 * W
189
+
190
+ if skiptriv: # local p copy for the qd*p rows
191
+ parts.append(PLSB + limb_str(p, W, base))
192
+ pos += 1 + W
193
+
194
+ if srt:
195
+ # SRT-style signed-digit division: qd in {-9..9} chosen by a top-4/
196
+ # top-3 leading-limbs lookup; remainder kept in (-p, p) as 10's-
197
+ # complement limbs. Redundancy absorbs lookup error: wherever the
198
+ # ratio sits near a digit boundary BOTH neighbours are valid
199
+ # (|num - qd*p| < p either way), so the knife-edge cases that broke
200
+ # exact-compare qd (0.96 ceiling, all errors off-by-one at margin
201
+ # ~1e-3) become don't-cares. Verified: 0 invariant violations /
202
+ # 500k selections, worst |s|/p = 0.54. num = 10*s + d in complement
203
+ # is a pure shift-append COPY (no arithmetic).
204
+ K = W + 2
205
+ r = N // base ** W # after the W trivial steps
206
+ for i in range(W, 2 * W):
207
+ d = n_msb[i]
208
+ num = r * base + d
209
+ parts.append(f"{i:02d}" + limb_char(d)) # stepi + cursor
210
+ pos += 3
211
+ num_c = to_limbs((num + base ** K) % base ** K, K, base)
212
+ parts.append("".join(limb_char(v) for v in num_c))
213
+ pos += K
214
+ qd = _srt_qd(num, p, base)
215
+ assert abs(num - qd * p) < p, (x, y, p, i, num, qd)
216
+ ann_g.append((pos, "qhat", qd + base - 1)) # 19-class signed label
217
+ parts.append(chr(0x90 + base - 1 + qd))
218
+ pos += 1
219
+ aq = abs(qd) * p
220
+ p_l = to_limbs(p, W, base) + [0]
221
+ c = 0
222
+ for k in range(W + 1): # carry into limb k of |qd|*p
223
+ ann_g.append((pos + k, "spcarry", c))
224
+ c = (abs(qd) * p_l[k] + c) // base
225
+ parts.append(limb_str(aq, W + 1, base))
226
+ pos += W + 1
227
+ s = num - qd * p
228
+ s_c = to_limbs((s + base ** K) % base ** K, K, base)
229
+ aq_l = to_limbs(aq, K, base)
230
+ ch = 0
231
+ for k in range(K): # (chain-bit, limb) pairs
232
+ parts.append(str(ch) + limb_char(s_c[k]))
233
+ if qd >= 0: # subtract: borrow chain
234
+ ch = 1 if num_c[k] - aq_l[k] - ch < 0 else 0
235
+ else: # negative digit: add carries
236
+ ch = (num_c[k] + aq_l[k] + ch) // base
237
+ pos += 2 * K
238
+ r = s
239
+ neg = 1 if r < 0 else 0 # final: ans = r + p if r<0
240
+ parts.append(str(neg))
241
+ pos += 1
242
+ r_c = to_limbs((r + base ** (W + 1)) % base ** (W + 1), W + 1, base)
243
+ pn_l = to_limbs(p * neg, W + 1, base)
244
+ answer = r + p * neg
245
+ a_l = to_limbs(answer, W, base)
246
+ ch = 0
247
+ for k in range(W): # (carry, limb) correction pairs
248
+ parts.append(str(ch) + limb_char(a_l[k]))
249
+ ch = (r_c[k] + pn_l[k] + ch) // base
250
+ pos += 2 * W
251
+ assert answer == N % p, (x, y, p)
252
+ q_limbs = [] # skip the classic loop below
253
+
254
+ if not srt:
255
+ q_limbs, rems, answer = long_division(N, p, W, base)
256
  r_prev = 0
257
+ for i, (qd, r) in enumerate(zip(q_limbs, rems) if not srt else ()):
258
+ if skiptriv and i < W: # provably qd=0 (Q < p): skip
259
+ assert qd == 0
260
+ r_prev = r
261
+ continue
262
+ if stepidx: # the step announces its own name
263
+ parts.append(f"{i:02d}")
264
+ pos += 2
265
  if cursor: # copy the consumed dividend limb
266
  parts.append(limb_char(n_msb[i]))
267
  pos += 1
268
+ if subnum: # numerator copy: r_prev*B + limb
269
+ num = r_prev * base + n_msb[i]
270
+ parts.append(limb_str(num, W + 1, base))
271
+ pos += W + 1
272
+ # Install the TRUE quotient digit, not the leading-limbs estimate: at
273
+ # tier 4 (10-digit p) qhat_estimate disagrees with qd in 46.6% of
274
+ # steps (top-2/top-1 degrades with divisor width), so the probe was
275
+ # supervising noise — the model reached qd 0.975 by correcting it,
276
+ # but the install should shape where the true digit is computed.
277
+ ann_g.append((pos, "qhat", qd))
278
  if not scratch: # compact: remainder is internal state
279
  rstr = to_limbs(r, W, base, msb_first=True)
280
  for j in range(W):
 
291
  parts.append(limb_str(qdp, W + 1, base))
292
  pos += W + 1
293
  if scratch: # emit remainder LSB-first (local borrow)
294
+ num_l = [n_msb[i]] + to_limbs(r_prev, W, base)
295
+ qdp_l = to_limbs(qd * p, W + 1, base)
296
+ if bemit: # (borrow, limb) pairs: chain in-context
297
+ r_l = to_limbs(r, W, base)
298
  b = 0
299
  for k in range(W):
300
+ parts.append(str(b) + limb_char(r_l[k]))
301
  b = 1 if num_l[k] - qdp_l[k] - b < 0 else 0
302
+ pos += 2 * W
303
+ else:
304
+ if subpad: # borrow chain of num - qd*p
305
+ b = 0
306
+ for k in range(W):
307
+ ann_g.append((pos + k, "sborrow", b))
308
+ b = 1 if num_l[k] - qdp_l[k] - b < 0 else 0
309
+ parts.append(limb_str(r, W, base))
310
+ pos += W
311
  r_prev = r
312
  parts.append(REVMARK)
313
  pos += 1
 
319
 
320
  prompt = prompt_str(x, y, p, W, base, subpad)
321
  text = prompt + "".join(parts)
322
+ assert len(text) == len(prompt) + gen_len(W, scratch, cursor, subpad, stepidx,
323
+ skiptriv, subnum, bemit, srt) + 1, \
324
+ (len(text), len(prompt), gen_len(W, scratch, cursor, subpad, stepidx,
325
+ skiptriv, subnum, bemit, srt))
326
  ann = [dict() for _ in range(len(text))]
327
  base_i = len(prompt)
328
  for gi, var, val in ann_g:
 
330
  return text, ann
331
 
332
 
333
+ def var_specs(W: int, base: int, scratch: bool = False, subpad: bool = False,
334
+ srt: bool = False):
335
+ specs = [("ans", base), ("qhat", 2 * base - 1 if srt else base),
336
+ ("acarry", base)]
337
  if subpad: # multiply-carry + subtract-borrow chains
338
  specs += [("spcarry", base), ("sborrow", 2)]
339
  if not scratch: # scratch emits remainders: no probe
 
342
 
343
 
344
  def decode_answer(gen_chars: str, W: int, base: int, scratch: bool = False,
345
+ cursor: bool = False, subpad: bool = False,
346
+ stepidx: bool = False, skiptriv: bool = False,
347
+ subnum: bool = False, bemit: bool = False,
348
+ srt: bool = False) -> int:
349
+ # the answer block after the R-mark is W plain limbs in every mode
350
+ off = gen_len(W, scratch, cursor, subpad, stepidx, skiptriv, subnum, bemit,
351
+ srt) - W
352
  return parse_limbs(gen_chars[off:off + W], base)
353
 
354
 
 
356
  import random
357
  rng = random.Random(3)
358
  for base in (10, 100):
359
+ for scratch, cursor, subpad, stepidx, skiptriv, subnum, bemit, srt in (
360
+ (False, False, False, False, False, False, False, False),
361
+ (True, False, False, False, False, False, False, False),
362
+ (True, True, False, False, False, False, False, False),
363
+ (True, True, True, False, False, False, False, False),
364
+ (True, True, True, True, False, False, False, False),
365
+ (True, True, True, True, True, False, False, False),
366
+ (True, True, True, True, True, True, False, False),
367
+ (True, True, True, True, True, True, True, False),
368
+ (True, True, True, True, True, True, True, True)):
369
+ if srt and base != 10:
370
+ continue
371
  for _ in range(4000):
372
+ W = rng.randint(1, 12)
373
  p = rng.randrange(max(2, base ** (W - 1)), base ** W)
374
  x, y = rng.randrange(p), rng.randrange(p)
375
+ text, ann = build_example(x, y, p, W, base, scratch, cursor,
376
+ subpad, stepidx, skiptriv, subnum, bemit,
377
+ srt)
378
  plen = len(prompt_str(x, y, p, W, base, subpad))
379
+ assert decode_answer(text[plen:], W, base, scratch, cursor,
380
+ subpad, stepidx, skiptriv, subnum, bemit,
381
+ srt) \
382
+ == (x * y) % p, (x, y, p, W, base, scratch, cursor, subpad,
383
+ stepidx, skiptriv, subnum, bemit, srt)
384
  assert len(ann) == len(text)
385
  print(f"composed base={base} scratch={scratch} cursor={cursor} "
386
+ f"subpad={subpad} stepidx={stepidx} skiptriv={skiptriv} "
387
+ f"subnum={subnum} bemit={bemit} srt={srt}: 4000/4000 decode OK")
388
  print("\ntokens/example (prompt+gen+NL):")
389
  for tier, Wd in [(3, 5), (4, 10), (5, 20), (6, 39), (7, 78)]:
390
  for base, W in ((10, Wd), (100, (Wd + 1) // 2)):