|
| 1 | +# Inline PC Memory Access into CPU |
| 2 | + |
| 3 | +## Goal |
| 4 | + |
| 5 | +Eliminate the MEMW_R table rows for the PC register by having the CPU chip interact directly with the low-level `memory` bus for PC reads and writes. This removes one MEMW_R row per CPU step, roughly halving the MEMW_R table size for most programs. |
| 6 | + |
| 7 | +## Context |
| 8 | + |
| 9 | +Currently, every CPU cycle writes `next_pc` to register x255 (address 510-511) via the `MEMW` bus. This routes through MEMW_R (register fast-path), which decomposes the 2-word register operation into two per-word interactions on the `memory` bus. Since the PC register has a completely predictable access pattern (read once, write once, every cycle, at known addresses), this intermediate step is unnecessary. |
| 10 | + |
| 11 | +Two buses are involved: |
| 12 | +- **`MEMW` bus**: High-level, 24-element fingerprint. CPU sends multi-byte operations; MEMW/MEMW_R receive and decompose. |
| 13 | +- **`memory` bus**: Low-level, 6-element fingerprint `(is_register, addr_lo, addr_hi, ts_lo, ts_hi, value)`. MEMW_R, MEMW_A, and PAGE tables interact here. Token-based memory argument: every read consumes a token (sender, +1), every write emits a token (receiver, -1). |
| 14 | + |
| 15 | +Based on spec PR #501 (`spec/inline-pc`). |
| 16 | + |
| 17 | +## Design |
| 18 | + |
| 19 | +### What gets removed |
| 20 | + |
| 21 | +**One MEMW bus interaction from CPU** (currently CM54): |
| 22 | +``` |
| 23 | +MEMW[pc; 1, 510, next_pc, timestamp+1, 1, 0, 0] multiplicity = (1 - pad) |
| 24 | +``` |
| 25 | +This 24-element sender to `BusId::Memw` with all-ALU-flags multiplicity. |
| 26 | + |
| 27 | +**One MEMW_R row per CPU cycle**: The trace builder currently creates a `MemwOperation` for the PC at every non-padding row. This routes through MEMW_R, producing one row there. |
| 28 | + |
| 29 | +### What gets added |
| 30 | + |
| 31 | +**Two new CPU columns** (74 → 76): |
| 32 | + |
| 33 | +| Column | Type | Description | |
| 34 | +|--------|------|-------------| |
| 35 | +| `PREV_PC_TIMESTAMP_BORROW` | Bit | Borrow bit for 64-bit subtraction `timestamp - 3`. Set when `timestamp_lo < 3`. | |
| 36 | +| `PC_DOUBLE_READ` | Bit | 1 when `rs1 = 255` (AUIPC/JAL reads PC via M1). Affects previous timestamp. | |
| 37 | + |
| 38 | +**Four new `memory` bus interactions** (replacing the one MEMW interaction): |
| 39 | + |
| 40 | +For `i = 0, 1` (two words of address 510 and 511): |
| 41 | + |
| 42 | +1. **PC read** (sender, +1 multiplicity, consumes old token): |
| 43 | + ``` |
| 44 | + memory[1, 510+i, 0, prev_ts_lo, prev_ts_hi, pc[i]] |
| 45 | + multiplicity = (1 - pad) |
| 46 | + ``` |
| 47 | + |
| 48 | +2. **PC write** (receiver, -1 multiplicity, emits new token): |
| 49 | + ``` |
| 50 | + memory[1, 510+i, 0, timestamp + 1, 0, next_pc[i]] |
| 51 | + multiplicity = -(1 - pad) |
| 52 | + ``` |
| 53 | + |
| 54 | +Where: |
| 55 | +``` |
| 56 | +prev_ts_lo = timestamp - 3*(1 - pc_double_read) + 2^32 * borrow |
| 57 | +prev_ts_hi = 0 - borrow |
| 58 | +``` |
| 59 | +(Since timestamps fit in 32 bits, `timestamp_hi = 0` always. The subtraction of 3 can only underflow in the lo word.) |
| 60 | + |
| 61 | +**Four new constraints**: |
| 62 | +1. `pc_double_read * (rs1 - 255) = 0` — pc_double_read=1 only when rs1=255 |
| 63 | +2. `pc_double_read * prev_pc_timestamp_borrow = 0` — no borrow when pc_double_read=1 (subtraction is 0) |
| 64 | +3. `IS_BIT[prev_pc_timestamp_borrow]` |
| 65 | +4. `IS_BIT[pc_double_read]` |
| 66 | + |
| 67 | +### Impact summary |
| 68 | + |
| 69 | +| Metric | Before | After | Delta | |
| 70 | +|--------|--------|-------|-------| |
| 71 | +| CPU main columns | 74 | 76 | +2 | |
| 72 | +| CPU MEMW bus interactions | 1 (24 elements) | 0 | -1 | |
| 73 | +| CPU memory bus interactions | 0 | 4 (6 elements each) | +4 | |
| 74 | +| CPU transition constraints | N | N+4 | +4 | |
| 75 | +| MEMW_R rows per CPU cycle | 1 | 0 | -1 | |
| 76 | + |
| 77 | +Net trace savings: for N CPU steps, we add 2N field elements to CPU but remove 10N from MEMW_R (10 columns). Net saving: 8N field elements (~6% total trace reduction for typical programs). |
| 78 | + |
| 79 | +## Timestamp Arithmetic |
| 80 | + |
| 81 | +The CPU stores `timestamp` as a single field element. The memory bus needs `(ts_lo, ts_hi)` where lo/hi are 32-bit words. Since timestamps never exceed 2^32 for practical programs, `ts_hi = 0` always. |
| 82 | + |
| 83 | +**Normal case** (`pc_double_read = 0`): The PC was last written at `T+1` in the previous cycle. With granularity 4, `T_prev + 1 = T - 3`: |
| 84 | +``` |
| 85 | +prev_ts_lo = timestamp - 3 + 2^32 * borrow (field arithmetic) |
| 86 | +prev_ts_hi = 0 - borrow = -borrow (0 or p-1 in the field, but bus expects 0 or -1 as word) |
| 87 | +``` |
| 88 | +When `timestamp < 3` (first cycle), `borrow = 1` corrects the underflow. |
| 89 | + |
| 90 | +**AUIPC/JAL case** (`pc_double_read = 1`): The M1 register read already consumed the previous PC token (via MEMW→MEMW_R→memory bus at timestamp T+0) and emitted a new one at T+0. The inline PC read consumes that fresh token: |
| 91 | +``` |
| 92 | +prev_ts_lo = timestamp (no subtraction) |
| 93 | +prev_ts_hi = 0 |
| 94 | +borrow = 0 (forced by constraint) |
| 95 | +``` |
| 96 | + |
| 97 | +**Token chain for AUIPC/JAL**: Previous write (T_prev+1) → MEMW_R read (T_prev+1) → MEMW_R write (T+0) → inline read (T+0) → inline write (T+1). Each sender (+1) at timestamp X cancels the receiver (-1) that emitted at timestamp X. |
| 98 | + |
| 99 | +## Files to Change |
| 100 | + |
| 101 | +### 1. `prover/src/tables/cpu.rs` |
| 102 | + |
| 103 | +**Column definitions** (`cols` module): |
| 104 | +- Add `PREV_PC_TIMESTAMP_BORROW` at index 74 |
| 105 | +- Add `PC_DOUBLE_READ` at index 75 |
| 106 | +- Update `NUM_COLUMNS` from 74 to 76 |
| 107 | + |
| 108 | +**Trace generation** (`generate_cpu_trace`): |
| 109 | +- For each row: compute `pc_double_read = (d.rs1 == 255 && d.read_register1) as u64` |
| 110 | +- Compute `prev_pc_timestamp_borrow = (op.timestamp < 3 && pc_double_read == 0) as u64` |
| 111 | +- Write both values to columns 74 and 75 |
| 112 | + |
| 113 | +**Bus interactions** (`cpu_bus_interactions`): |
| 114 | +- Remove CM54 (the MEMW sender for PC) |
| 115 | +- Add 4 memory bus interactions (2 reads + 2 writes to `BusId::Memory`) |
| 116 | + |
| 117 | +### 2. `prover/src/constraints/cpu.rs` |
| 118 | + |
| 119 | +- Add constraint: `pc_double_read * (rs1 - 255) = 0` |
| 120 | +- Add constraint: `pc_double_read * prev_pc_timestamp_borrow = 0` |
| 121 | +- Add IS_BIT constraints for both new columns |
| 122 | + |
| 123 | +### 3. `prover/src/tables/trace_builder.rs` |
| 124 | + |
| 125 | +- In `collect_memw_from_cpu` (lines 594-607): Remove the CM54 block that creates a `MemwOperation` for the PC register |
| 126 | +- Keep `register_state.write_pc(op.next_pc, op.timestamp + 1)` — the register state tracker is still needed for M1 interactions when `rs1=255` (AUIPC/JAL), which still go through MEMW_R |
| 127 | + |
| 128 | +### 4. `prover/src/test_utils.rs` |
| 129 | + |
| 130 | +- Update `create_cpu_air` to use 76 columns |
| 131 | +- Add new bus interactions and constraints to the CPU AIR |
| 132 | + |
| 133 | +## Testing |
| 134 | + |
| 135 | +1. **Existing prove-and-verify tests**: All passing tests must continue to pass. The memory bus must still balance globally. |
| 136 | +2. **AUIPC/JAL**: Test programs using `rs1=255` to verify the `pc_double_read` path and token chain correctness. |
| 137 | +3. **First cycle**: Verify `timestamp < 3` triggers the borrow bit correctly. |
| 138 | +4. **Bus balance**: The global LogUp sum must still be zero (or the expected commit offset). |
0 commit comments