Skip to content

Commit 856d281

Browse files
committed
Make simple calculator columns MSB-first
1 parent 258959f commit 856d281

2 files changed

Lines changed: 46 additions & 37 deletions

File tree

docs/calculator_simple_explained.md

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ columns, and makes one carry pass across the column totals.
3838

3939
### 1. Build the Times Table
4040

41-
The code starts by constructing the 100 entries in the `0–9` times table. Each
42-
product is stored as separate tens and ones:
41+
The code starts by constructing a 100-entry Python mapping for the `0–9` times
42+
table. Each product is stored as separate tens and ones:
4343

4444
```python
4545
for a in range(10):
@@ -53,8 +53,9 @@ for a in range(10):
5353
```
5454

5555
For example, `7 × 8` is stored as `(5, 6)`. This Python loop runs once while
56-
building the calculator; the resulting table becomes a fixed part of the
57-
finished model.
56+
building the graph. Every later call to `onehot_lookup` materializes the
57+
mapping as an independent 100-lane FFN node; the finished transformer does not
58+
contain one shared table queried by every digit pair.
5859

5960
### 2. Place the Products into Columns
6061

@@ -67,49 +68,57 @@ product = onehot_lookup(
6768
tens = _slice(product, 0, 1, name="product_tens")
6869
ones = _slice(product, 1, 1, name="product_ones")
6970

70-
place = (n - 1 - i) + (n - 1 - j)
71-
columns[place].append(ones)
72-
columns[place + 1].append(tens)
71+
columns[i + j].append(tens)
72+
columns[i + j + 1].append(ones)
7373
```
7474

7575
Each pair gets its own lookup, so all the digit products can be found together.
76-
For the default three-digit calculator, there are `3 × 3 = 9` of these lookups.
77-
The ones and tens are then sent to the columns representing their place values.
76+
For the default three-digit calculator, there are `3 × 3 = 9` independent
77+
lookup circuits, comprising 900 FFN lanes. The operands, columns, and result
78+
are all stored most-significant digit first. For operands at positions `i` and
79+
`j`, the product's tens go into column `i + j` and its ones go into the next
80+
column, `i + j + 1`.
7881

79-
For `12 × 34`, the work looks like this:
82+
For `12 × 34`, the nonzero columns look like this from most significant to
83+
least significant:
8084

8185
| Column | Contributions | Result |
8286
| --- | --- | --- |
83-
| Ones | `2 × 4` contributes `8` | Write `8` |
84-
| Tens | `2 × 3` contributes `6`; `1 × 4` contributes `4` | Write `0`, carry `1` |
8587
| Hundreds | `1 × 3` contributes `3`, plus the carry | Write `4` |
88+
| Tens | `2 × 3` contributes `6`; `1 × 4` contributes `4` | Write `0`, carry `1` |
89+
| Ones | `2 × 4` contributes `8` | Write `8` |
8690

8791
### 3. Make One Carry Pass
8892

8993
The last multiplication step moves from the least-significant column to the
9094
most-significant one. The essential part of the code is:
9195

9296
```python
93-
total = add(sum_nodes(contributions), carry)
94-
total_onehot = bool_to_01(
95-
in_range(total, add_const(total, 1.0), max_total + 1)
96-
)
97-
out_lsb_first.append(
98-
onehot_lookup(total_onehot, digit_table, embedding.get_embedding("0"))
99-
)
100-
carry = piecewise_linear(
101-
total,
102-
sorted(carry_knots),
103-
carry_knots.__getitem__,
104-
input_scale=step_sharpness,
105-
name="carry_staircase",
106-
)
97+
for column in reversed(columns):
98+
contributions = column or [zero_scalar]
99+
total = add(sum_nodes(contributions), carry)
100+
total_onehot = bool_to_01(
101+
in_range(total, add_const(total, 1.0), max_total + 1)
102+
)
103+
out_lsb_first.append(
104+
onehot_lookup(total_onehot, digit_table, embedding.get_embedding("0"))
105+
)
106+
carry = piecewise_linear(
107+
total,
108+
sorted(carry_knots),
109+
carry_knots.__getitem__,
110+
input_scale=step_sharpness,
111+
name="carry_staircase",
112+
)
107113
```
108114

109-
For each column, `total` combines its contributions with the incoming carry.
110-
The lookup selects the final decimal digit, while `carry_staircase` calculates
111-
how many tens move into the next column. This stage is sequential because each
112-
column needs the carry produced by the column to its right.
115+
Although `columns` is stored most-significant first, carrying must proceed from
116+
right to left, so the loop traverses `reversed(columns)`. For each column,
117+
`total` combines its contributions with the incoming carry. The lookup selects
118+
the final decimal digit, while `carry_staircase` calculates how many tens move
119+
into the next column. This stage is sequential because each column needs the
120+
carry produced by the column to its right. The loop accumulates the output
121+
least-significant digit first, then reverses it before returning the result.
113122

114123
The result is held at a fixed width, so `12 × 34` may initially appear as
115124
`000408`. The leading zeros are removed later.

examples/calculator_simple.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -248,8 +248,9 @@ def multiply_digit_seqs(
248248
default_product = torch.tensor([0.0, 0.0])
249249

250250
# Steps 1-2: drop each product's digits into their place-value columns and
251-
# collect the (free) per-column number contributions. Column p is the
252-
# 10^p place; columns run 0 (least significant) .. 2n-1.
251+
# collect the (free) per-column number contributions. Like the operands
252+
# and result, columns are MSB-first: column 0 is the most significant
253+
# product digit and column 2n-1 is the units digit.
253254
columns: list[list[Node]] = [[] for _ in range(2 * n)]
254255
for i in range(n):
255256
for j in range(n):
@@ -258,9 +259,8 @@ def multiply_digit_seqs(
258259
)
259260
tens = _slice(product, 0, 1, name="product_tens")
260261
ones = _slice(product, 1, 1, name="product_ones")
261-
place = (n - 1 - i) + (n - 1 - j) # place value of the ones digit
262-
columns[place].append(ones)
263-
columns[place + 1].append(tens)
262+
columns[i + j].append(tens)
263+
columns[i + j + 1].append(ones)
264264

265265
# Step 3: one carry sweep, least-significant column first. Turn each
266266
# column total (a number 0..20n) into a one-hot index and read off its
@@ -292,8 +292,8 @@ def multiply_digit_seqs(
292292

293293
carry: Node = zero_scalar
294294
out_lsb_first: list[Node] = []
295-
for place in range(2 * n):
296-
contributions = columns[place] or [zero_scalar]
295+
for column in reversed(columns):
296+
contributions = column or [zero_scalar]
297297
total = add(sum_nodes(contributions), carry) # free number addition
298298
total_onehot = bool_to_01(in_range(total, add_const(total, 1.0), max_total + 1))
299299
out_lsb_first.append(

0 commit comments

Comments
 (0)