Skip to main content

rhdl_ubft/kv/
hash_table.rs

1//! Transactional cuckoo table, nine true synchronous RAM banks of 64 entries.
2//! Transactions are serialized; responses hold until consumed. Reset discards all
3//! in-flight work and scrubs every RAM address before accepting another request.
4//!
5//! The central invariant is that no request can observe an unfinished eviction.
6//! A successful insertion keeps its writes; an exhausted insertion restores
7//! every overwritten entry before publishing failure. Holding the response
8//! also holds off the next request, which makes the transaction boundary clear.
9//!
10//! State encoding: 0 scrub, 1 idle/admit, 2 launch RAM addresses, 3 inspect RAM
11//! results, 4 undo one journal entry. New requests launch their RAM addresses
12//! on admission. Evicted victims still use state 2 to launch their new address.
13//! Reset clears the table; it does not restore a persistent pre-reset snapshot.
14//!
15//! Transfers occur on rising edges with valid and ready asserted. Producers
16//! retain valid and payload until transfer. Simultaneous lookup/update requests
17//! alternate admission preference; a stalled response blocks further admission.
18//! Reset suppresses request and response handshakes.
19use super::tabulation::hashes;
20use rhdl::prelude::*;
21use rhdl_fpga::core::{
22    dff::DFF,
23    ram::synchronous::{RamStyle, SyncBRAM},
24};
25
26#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
27/// One lookup; hold its fields until `valid && lookup_request_ready`.
28pub struct LookupReq {
29    /// A lookup is offered; hold key/source until its request handshake.
30    pub valid: bool,
31    /// Full 64-bit key; zero is a valid key.
32    pub key: b64,
33    /// Opaque caller bit echoed in the response; it grants no access rights.
34    pub source: bool,
35}
36#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
37/// A held result. A miss always returns value zero; consult `hit` to distinguish
38/// that miss from a stored zero value.
39pub struct LookupResp {
40    /// Response pending; retained until lookup_response_ready.
41    pub valid: bool,
42    /// Original request key.
43    pub key: b64,
44    /// Stored value on hit, otherwise zero.
45    pub value: b16,
46    /// Original request source bit, echoed without interpretation.
47    pub source: bool,
48    /// A valid entry matched the full key.
49    pub hit: bool,
50}
51/// op=0 upsert, op=1 delete. This is a semantic API, not HLS enum packing.
52#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
53pub struct UpdateReq {
54    /// An update is offered; hold all fields until its request handshake.
55    pub valid: bool,
56    /// Zero upserts; one deletes all matching candidates.
57    pub op: b1,
58    /// Full 64-bit key to update.
59    pub key: b64,
60    /// Replacement value for upsert; ignored for deletion but echoed.
61    pub value: b16,
62    /// Opaque caller bit echoed unchanged; not authorization.
63    pub source: bool,
64}
65#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
66/// Upsert success means the entry is installed. Delete success means an entry
67/// existed. Failed upsert means all eviction writes have already been undone.
68pub struct UpdateResp {
69    /// Response pending; retained until update_response_ready.
70    pub valid: bool,
71    /// Original requested operation.
72    pub op: b1,
73    /// Original request key, never a displaced victim key.
74    pub key: b64,
75    /// Original request value, including for deletion and failed insertion.
76    pub value: b16,
77    /// Original request source bit.
78    pub source: bool,
79    /// Upsert installed, or deletion found an entry; false otherwise.
80    pub success: bool,
81}
82#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
83/// Requests and response-consumer readiness for the serialized table.
84pub struct HashTableIn {
85    /// Lookup request channel.
86    pub lookup: LookupReq,
87    /// Upsert/delete request channel.
88    pub update: UpdateReq,
89    /// Consumer accepts a valid lookup response this edge.
90    pub lookup_response_ready: bool,
91    /// Consumer accepts a valid update response this edge.
92    pub update_response_ready: bool,
93}
94#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
95/// Admission readiness, held responses, and reset/diagnostic status.
96pub struct HashTableOut {
97    /// Admits a valid lookup this edge; depends on arbitration and response drain.
98    pub lookup_request_ready: bool,
99    /// Admits a valid update this edge; depends on arbitration and response drain.
100    pub update_request_ready: bool,
101    /// Held lookup result.
102    pub lookup: LookupResp,
103    /// Held update result.
104    pub update: UpdateResp,
105    /// Wrapping diagnostic counter, not a transaction identifier.
106    pub insert_failures: b16,
107    /// The reset scrub has completed; requests may now be admitted.
108    pub initialized: bool,
109}
110#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
111/// One physical RAM entry; key and value are meaningful only while valid.
112pub struct Entry {
113    /// Occupancy bit; false marks a free location.
114    pub valid: bool,
115    /// Resident full-width key.
116    pub key: b64,
117    /// Resident application value.
118    pub value: b16,
119}
120#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
121/// The old contents of one physical location, recorded before overwriting it.
122/// Repeated locations remain separate records because rollback is ordered.
123pub struct Undo {
124    /// Physical bank index, constrained to 0 through 8.
125    pub bank: b4,
126    /// Six-bit address within the bank.
127    pub addr: b6,
128    /// Complete contents before this overwrite, including validity.
129    pub entry: Entry,
130}
131#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
132/// Transaction controller state, including the twelve-entry undo journal.
133pub struct Registers {
134    state: b3,
135    scrub: b6,
136    initialized: bool,
137    prefer_update: bool,
138    is_update: bool,
139    // Original caller request remains unchanged throughout an eviction chain.
140    request: UpdateReq,
141    // Entry seeking a home: initially the request, then each displaced victim.
142    pending: Entry,
143    lookup: LookupResp,
144    update: UpdateResp,
145    failures: b16,
146    journal: [Undo; 12],
147    // Number of journal records written, or still to restore during rollback.
148    count: b4,
149    victim_index: b4,
150    victim_bit: bool,
151}
152#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
153/// Nine 64-entry synchronous RAM banks with key64/value16 and bounded insertion.
154/// Reset scrubs all contents. Failed insertion rolls back before reporting failure.
155pub struct HashTable {
156    regs: DFF<Registers>,
157    banks: [SyncBRAM<Entry, 6>; 9],
158}
159impl Default for HashTable {
160    fn default() -> Self {
161        Self {
162            regs: DFF::new(Registers::default()),
163            banks: std::array::from_fn(|_| SyncBRAM::default().with_style(RamStyle::Block)),
164        }
165    }
166}
167impl SynchronousIO for HashTable {
168    type I = HashTableIn;
169    type O = HashTableOut;
170    type Kernel = hash_table_kernel;
171}
172
173/// Advance admission, synchronous RAM access, or one rollback step.
174/// Decisions read the registered snapshot; writes describe the next edge.
175#[kernel]
176pub fn hash_table_kernel(
177    cr: ClockReset,
178    i: HashTableIn,
179    q: HashTableQ,
180) -> (HashTableOut, HashTableD) {
181    // q is the registered state for this cycle; d/r describe the next edge.
182    // Read q for decisions so one transition cannot cascade through two states.
183    let mut d = HashTableD::dont_care();
184    let mut r = q.regs;
185    // Consuming the old response frees its slot on this edge. Admission may
186    // share that edge, but a stalled response still owns the transaction.
187    let idle = q.regs.state == b3(1)
188        && (!q.regs.lookup.valid || i.lookup_response_ready)
189        && (!q.regs.update.valid || i.update_response_ready)
190        && !cr.reset.any();
191    // With both callers waiting, the previous winner yields to the other.
192    // With only one caller, no preference bit can unnecessarily block it.
193    let accept_update = idle && (!i.lookup.valid || q.regs.prefer_update);
194    let accept_lookup = idle && (!i.update.valid || !q.regs.prefer_update);
195    // Select the key before hashing, so admission reuses the same nine hash
196    // networks as eviction. No writes occur in the admission state.
197    let mut read_key = q.regs.pending.key;
198    if accept_update && i.update.valid {
199        read_key = i.update.key;
200    } else if accept_lookup && i.lookup.valid {
201        read_key = i.lookup.key;
202    }
203    let addresses = hashes(read_key);
204    for bank in 0..9 {
205        d.banks[bank].read_addr = addresses[bank];
206        d.banks[bank].write.addr = addresses[bank];
207        d.banks[bank].write.value = q.regs.pending;
208        d.banks[bank].write.enable = false;
209    }
210    let mut o = HashTableOut {
211        lookup_request_ready: accept_lookup,
212        update_request_ready: accept_update,
213        lookup: q.regs.lookup,
214        update: q.regs.update,
215        insert_failures: q.regs.failures,
216        initialized: q.regs.initialized && !cr.reset.any(),
217    };
218    if i.lookup_response_ready {
219        r.lookup.valid = false;
220    }
221    if i.update_response_ready {
222        r.update.valid = false;
223    }
224    if !cr.reset.any() {
225        if q.regs.state == b3(0) {
226            // Clear all banks in parallel, one address per cycle. No BRAM reset
227            // inference is assumed, including when reset interrupts eviction.
228            for bank in 0..9 {
229                d.banks[bank].write.addr = q.regs.scrub;
230                d.banks[bank].write.value = Entry {
231                    valid: false,
232                    key: b64(0),
233                    value: b16(0),
234                };
235                d.banks[bank].write.enable = true;
236            }
237            r.scrub = q.regs.scrub + b6(1);
238            if q.regs.scrub == b6(63) {
239                r.state = b3(1);
240                r.initialized = true;
241            }
242        } else if q.regs.state == b3(1) {
243            if accept_update && i.update.valid {
244                r.request = i.update;
245                r.pending = Entry {
246                    valid: true,
247                    key: i.update.key,
248                    value: i.update.value,
249                };
250                r.is_update = true;
251                r.count = b4(0);
252                r.victim_index = b4(0);
253                r.prefer_update = false;
254                // Read the accepted key immediately. The preceding operation's
255                // RAM write has already committed before this admission edge.
256                r.state = b3(3);
257            } else if accept_lookup && i.lookup.valid {
258                r.request = UpdateReq {
259                    valid: true,
260                    op: b1(0),
261                    key: i.lookup.key,
262                    value: b16(0),
263                    source: i.lookup.source,
264                };
265                r.pending = Entry {
266                    valid: true,
267                    key: i.lookup.key,
268                    value: b16(0),
269                };
270                r.is_update = false;
271                r.prefer_update = true;
272                r.state = b3(3);
273            }
274        } else if q.regs.state == b3(2) {
275            // Address launch; RAM result is consumed the following cycle.
276            r.state = b3(3);
277        } else if q.regs.state == b3(3) {
278            let mut hit = false;
279            let mut hit_bank = b4(0);
280            let mut value = b16(0);
281            let mut free = false;
282            let mut free_bank = b4(0);
283            for bank in 0..9 {
284                if q.banks[bank].valid && q.banks[bank].key == q.regs.pending.key {
285                    hit = true;
286                    hit_bank = b4(bank as u128);
287                    value = q.banks[bank].value;
288                }
289                if !q.banks[bank].valid {
290                    free = true;
291                    free_bank = b4(bank as u128);
292                }
293            }
294            if !q.regs.is_update {
295                r.lookup = LookupResp {
296                    valid: true,
297                    key: q.regs.request.key,
298                    value,
299                    source: q.regs.request.source,
300                    hit,
301                };
302                r.state = b3(1);
303            } else if q.regs.request.op == b1(1) {
304                for bank in 0..9 {
305                    if q.banks[bank].valid && q.banks[bank].key == q.regs.pending.key {
306                        d.banks[bank].write.enable = true;
307                        d.banks[bank].write.value.valid = false;
308                    }
309                }
310                r.update = UpdateResp {
311                    valid: true,
312                    op: q.regs.request.op,
313                    key: q.regs.request.key,
314                    value: q.regs.request.value,
315                    source: q.regs.request.source,
316                    success: hit,
317                };
318                r.state = b3(1);
319            } else if hit || free {
320                // Search every candidate for an existing key before choosing a
321                // free slot. Otherwise an upsert could create a second copy.
322                let chosen = if hit { hit_bank } else { free_bank };
323                for bank in 0..9 {
324                    if chosen == b4(bank as u128) {
325                        d.banks[bank].write.enable = true;
326                    }
327                }
328                r.update = UpdateResp {
329                    valid: true,
330                    op: q.regs.request.op,
331                    key: q.regs.request.key,
332                    value: q.regs.request.value,
333                    source: q.regs.request.source,
334                    success: true,
335                };
336                r.state = b3(1);
337            } else {
338                // All candidates are occupied. Save the victim before writing
339                // pending in its place, then seek a home for that victim.
340                // The twelfth displacement still needs a journal record even
341                // though the next state immediately begins rolling it back.
342                let mut victim = b4(0);
343                for bank in 0..9 {
344                    if q.regs.victim_index == b4(bank as u128) {
345                        victim = addresses[bank].resize::<4>() & b4(7);
346                    }
347                }
348                if q.regs.victim_bit {
349                    victim = victim + b4(1);
350                }
351                let mut undo = Undo {
352                    bank: victim,
353                    addr: b6(0),
354                    entry: q.regs.pending,
355                };
356                for bank in 0..9 {
357                    if victim == b4(bank as u128) {
358                        undo.addr = addresses[bank];
359                        undo.entry = q.banks[bank];
360                        d.banks[bank].write.enable = true;
361                        r.pending = q.banks[bank];
362                    }
363                }
364                for slot in 0..12 {
365                    if q.regs.count == b4(slot as u128) {
366                        r.journal[slot] = undo;
367                    }
368                }
369                r.count = q.regs.count + b4(1);
370                r.victim_bit = !q.regs.victim_bit;
371                r.victim_index = if q.regs.victim_index == b4(8) {
372                    b4(0)
373                } else {
374                    q.regs.victim_index + b4(1)
375                };
376                r.state = if q.regs.count == b4(11) { b3(4) } else { b3(2) };
377            }
378        } else if q.regs.state == b3(4) {
379            // Reverse order is essential when a failed path revisits an address.
380            let last = q.regs.count - b4(1);
381            for slot in 0..12 {
382                if last == b4(slot as u128) {
383                    for bank in 0..9 {
384                        if q.regs.journal[slot].bank == b4(bank as u128) {
385                            d.banks[bank].write.addr = q.regs.journal[slot].addr;
386                            d.banks[bank].write.value = q.regs.journal[slot].entry;
387                            d.banks[bank].write.enable = true;
388                        }
389                    }
390                }
391            }
392            r.count = last;
393            if last == b4(0) {
394                r.failures = q.regs.failures + b16(1);
395                r.update = UpdateResp {
396                    valid: true,
397                    op: q.regs.request.op,
398                    key: q.regs.request.key,
399                    value: q.regs.request.value,
400                    source: q.regs.request.source,
401                    success: false,
402                };
403                r.state = b3(1);
404            }
405        }
406    } else {
407        o.lookup.valid = false;
408        o.update.valid = false;
409    }
410    d.regs = r;
411    (o, d)
412}
413
414#[cfg(test)]
415#[path = "tests/hash_table.rs"]
416mod tests;