Skip to main content

logicaffeine_compile/codegen_sva/
crdt_rtl.rs

1//! crdt_rtl — the CRDT merge unit in silicon.
2//!
3//! A CRDT's convergence is a **join-semilattice merge**: for a G-Counter the join is the
4//! per-replica `max` (and its value is the sum of the merged lanes). That merge is exactly
5//! what `logicaffeine_data::crdt::GCounter::merge` computes in software. This module emits a
6//! fixed-width (4 replicas × 32 bit) hardware merge unit and proves, under iverilog, that
7//! its output is byte-identical to that oracle — so a replica's state converges the same way
8//! whether it merged on the CPU runtime or in the chip's coprocessor.
9//!
10//! It is *one node's* merge (the commutative/associative/idempotent join). The multi-node
11//! distribution protocol (who exchanges state with whom) stays the message-passing layer —
12//! honestly a projection, not this block.
13
14/// The join for one lane — per-replica `max`, exactly `GCounter::merge`.
15pub fn merge_lane(a: u32, b: u32) -> u32 {
16    a.max(b)
17}
18
19/// Emit the combinational G-Counter merge unit: `merged[i] = max(a[i], b[i])` across the
20/// four 32-bit replica lanes, plus `value = sum(merged)` (34 bits: 32 + ceil(log2 4)).
21pub fn emit_crdt_merge_verilog() -> String {
22    r#"// crdt_merge — the CRDT (G-Counter) join-semilattice merge in silicon.
23// merged[i] = max(a[i], b[i]) per replica lane; value = sum(merged).
24// Byte-identical to logicaffeine_data::crdt::GCounter::merge (proven by
25// crdt_merge_unit_matches_the_gcounter_oracle). 4 replicas x 32-bit.
26module crdt_merge(
27  input  [127:0] a,        // {a3,a2,a1,a0}, one 32-bit count per replica
28  input  [127:0] b,
29  output [127:0] merged,   // per-replica max (the lattice join)
30  output [33:0]  value     // sum of the four merged lanes
31);
32  wire [31:0] a0 = a[31:0],  a1 = a[63:32],  a2 = a[95:64],  a3 = a[127:96];
33  wire [31:0] b0 = b[31:0],  b1 = b[63:32],  b2 = b[95:64],  b3 = b[127:96];
34  wire [31:0] m0 = (a0 >= b0) ? a0 : b0;
35  wire [31:0] m1 = (a1 >= b1) ? a1 : b1;
36  wire [31:0] m2 = (a2 >= b2) ? a2 : b2;
37  wire [31:0] m3 = (a3 >= b3) ? a3 : b3;
38  assign merged = {m3, m2, m1, m0};
39  assign value  = m0 + m1 + m2 + m3;   // 34-bit LHS ⇒ no truncation
40endmodule
41"#
42    .to_string()
43}
44
45/// Emit the **DPU near-data ingest tile**: the same per-lane join as `crdt_merge`, but
46/// wrapped in a clocked accumulator — on each `posedge clk` with `in_valid`, the state
47/// folds the arriving beat into the running join (`acc <= merge(acc, in_state)`); `rst`
48/// clears it. This is the sequential *merge-on-ingest* primitive a DPU runs at the data
49/// edge: because the join is commutative/associative/idempotent (certified in
50/// `the_crdt_convergence_order_is_a_total_order_proven_by_our_sat`), folding a stream
51/// through it is **order-invariant** — correct even when the controller delivers reads
52/// out of logical order. Unlike the combinational `crdt_merge` unit, this has genuine
53/// register-to-register paths, so it is the block that carries meaning through STA/signoff.
54pub fn emit_dpu_ingest_verilog() -> String {
55    r#"// dpu_ingest — the DPU near-data compute tile: sequential CRDT merge-on-ingest.
56// On each posedge clk with in_valid, acc <= per-replica-max(acc, in_state) — the same
57// join-semilattice merge as crdt_merge (proven == GCounter::merge), now folded over a
58// stream. Order-invariant by the certified lattice laws, so it is correct even when the
59// deterministic controller delivers ingest beats out of logical order. 4 replicas x 32-bit.
60module dpu_ingest(
61  input          clk,
62  input          rst,        // synchronous clear of the accumulator
63  input          in_valid,   // a beat of near-data arrived this cycle
64  input  [127:0] in_state,   // {s3,s2,s1,s0}, one 32-bit count per replica
65  output [127:0] state,      // the accumulated join so far
66  output [33:0]  value       // sum of the four accumulated lanes
67);
68  reg  [127:0] acc;
69  wire [31:0] a0 = acc[31:0],      a1 = acc[63:32],      a2 = acc[95:64],      a3 = acc[127:96];
70  wire [31:0] b0 = in_state[31:0], b1 = in_state[63:32], b2 = in_state[95:64], b3 = in_state[127:96];
71  wire [31:0] m0 = (a0 >= b0) ? a0 : b0;
72  wire [31:0] m1 = (a1 >= b1) ? a1 : b1;
73  wire [31:0] m2 = (a2 >= b2) ? a2 : b2;
74  wire [31:0] m3 = (a3 >= b3) ? a3 : b3;
75  always @(posedge clk) begin
76    if (rst)           acc <= 128'd0;
77    else if (in_valid) acc <= {m3, m2, m1, m0};
78  end
79  assign state = acc;
80  assign value = acc[31:0] + acc[63:32] + acc[95:64] + acc[127:96];  // 34-bit LHS ⇒ no truncation
81endmodule
82"#
83    .to_string()
84}
85
86/// A self-checking testbench: apply N (a,b) vectors and compare `merged`/`value` against the
87/// oracle from `crdt_{a,b,m}.hex` (computed in Rust). Prints PASS/FAIL.
88pub fn emit_crdt_merge_testbench(n: usize) -> String {
89    format!(
90        r#"// tb_crdt_merge — HW G-Counter merge vs the software CRDT oracle (per-replica max).
91`timescale 1ns/1ps
92module tb_crdt_merge;
93  reg  [127:0] a, b;
94  reg  [127:0] oa [0:{last}];
95  reg  [127:0] ob [0:{last}];
96  reg  [127:0] om [0:{last}];
97  wire [127:0] merged;
98  wire [33:0]  value;
99  reg  [33:0]  ev;
100  integer i, errors = 0;
101  crdt_merge dut(.a(a), .b(b), .merged(merged), .value(value));
102  initial begin
103    $readmemh("crdt_a.hex", oa);
104    $readmemh("crdt_b.hex", ob);
105    $readmemh("crdt_m.hex", om);
106    for (i = 0; i < {n}; i = i + 1) begin
107      a = oa[i]; b = ob[i]; #1;
108      ev = {{2'b0, om[i][31:0]}} + {{2'b0, om[i][63:32]}} + {{2'b0, om[i][95:64]}} + {{2'b0, om[i][127:96]}};
109      if (merged !== om[i]) begin errors = errors + 1; $display("FAIL merged %0d", i); end
110      if (value  !== ev)    begin errors = errors + 1; $display("FAIL value %0d", i); end
111    end
112    if (errors == 0) $display("PASS: {n} CRDT merges (per-replica max join + value) match the software oracle");
113    else $display("FAIL: %0d CRDT merge mismatches", errors);
114    $finish;
115  end
116endmodule
117"#,
118        last = n - 1,
119        n = n,
120    )
121}
122
123/// Reproducible test vectors (a, b) and their oracle merge m = per-lane max — a plain LCG so
124/// the sequence is stable and needs no external crate.
125fn crdt_vectors(n: usize) -> (String, String, String) {
126    let mut st: u64 = 0x00C0_FFEE_D0D0_CAFE;
127    let mut next = || {
128        st = st
129            .wrapping_mul(6364136223846793005)
130            .wrapping_add(1442695040888963407);
131        (st >> 33) as u32
132    };
133    let (mut ha, mut hb, mut hm) = (String::new(), String::new(), String::new());
134    for _ in 0..n {
135        let a = [next(), next(), next(), next()];
136        let b = [next(), next(), next(), next()];
137        let m = [
138            merge_lane(a[0], b[0]),
139            merge_lane(a[1], b[1]),
140            merge_lane(a[2], b[2]),
141            merge_lane(a[3], b[3]),
142        ];
143        // {lane3,lane2,lane1,lane0} — lane3 is the most-significant 32 bits (bits [127:96]).
144        ha.push_str(&format!(
145            "{:08x}{:08x}{:08x}{:08x}\n",
146            a[3], a[2], a[1], a[0]
147        ));
148        hb.push_str(&format!(
149            "{:08x}{:08x}{:08x}{:08x}\n",
150            b[3], b[2], b[1], b[0]
151        ));
152        hm.push_str(&format!(
153            "{:08x}{:08x}{:08x}{:08x}\n",
154            m[3], m[2], m[1], m[0]
155        ));
156    }
157    (ha, hb, hm)
158}
159
160#[cfg(test)]
161#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/codegen_sva/crdt_rtl/tests.rs"]
162mod tests;