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;