Skip to main content

kv_store/
kv_store.rs

1//! A fixed-capacity key-value store whose every operation is an exact circuit:
2//! the store's state goes in, the next state and the answer come out.
3//!
4//! A `get` compares the key with every slot and picks the matching value,
5//! which is hard attention: a one-hot match over keys, multiplied into the
6//! values. A `put` overwrites the matching slot, or fills the first empty one,
7//! as a select per slot. The function below is plain Rust, and the circuit is
8//! checked against it.
9//!
10//! cargo run --release -p handwired-rs-gpu --example kv_store
11
12use std::time::Instant;
13
14use handwired_rs::TensorExecutor;
15use handwired_rs::dsl::Params;
16use handwired_rs::handwired;
17use handwired_rs_gpu::{Gpu, GpuBatch};
18
19const SLOTS: usize = 16;
20const GET: u8 = 0;
21const PUT: u8 = 1;
22const DELETE: u8 = 2;
23
24/// One operation on the store. A key of 0 marks an empty slot. Returns the
25/// next keys and values, whether the key was present, and its value before
26/// the operation.
27///
28/// Keys are declared within 0..15 here so that random tests hit present keys
29/// often; any range within u16 works.
30#[handwired]
31fn kv_step(
32    #[range(0, 15)] keys: [u16; 16],
33    values: [u16; 16],
34    #[range(0, 2)] op: u8,
35    #[range(1, 15)] key: u16,
36    value: u16,
37) -> ([u16; 16], [u16; 16], bool, u16) {
38    let mut keys = keys;
39    let mut values = values;
40    let mut found = false;
41    let mut previous: u16 = 0;
42    for slot in 0..16 {
43        if keys[slot] == key {
44            found = true;
45            previous = values[slot];
46            if op == PUT {
47                values[slot] = value;
48            }
49            if op == DELETE {
50                keys[slot] = 0;
51                values[slot] = 0;
52            }
53            break;
54        }
55    }
56    if op == PUT && !found {
57        // Insert into the first empty slot; a full store drops the write.
58        for slot in 0..16 {
59            if keys[slot] == 0 {
60                keys[slot] = key;
61                values[slot] = value;
62                break;
63            }
64        }
65    }
66    (keys, values, found, previous)
67}
68
69type State = ([u16; SLOTS], [u16; SLOTS]);
70type Operation = ([u16; SLOTS], [u16; SLOTS], u8, u16, u16);
71
72struct Rng(u64);
73impl Rng {
74    fn next(&mut self) -> u64 {
75        self.0 ^= self.0 << 13;
76        self.0 ^= self.0 >> 7;
77        self.0 ^= self.0 << 17;
78        self.0
79    }
80    fn below(&mut self, bound: u64) -> u64 {
81        self.next() % bound
82    }
83}
84
85fn main() {
86    let start = Instant::now();
87    let compiled = kv_step_circuit();
88    println!(
89        "kv_step: {} live operations in {} layers, built in {:.1?}",
90        compiled.program.live_operation_count(),
91        compiled.program.layers.len(),
92        start.elapsed()
93    );
94    let report = compiled.verify(20_000).unwrap();
95    println!("verified against the function on {} sampled inputs", report.checked);
96
97    // A session: the circuit holds the store's state from one operation to the next, like a database.
98    let mut executor = TensorExecutor::new(&compiled.program);
99    let mut circuit_state: State = ([0; SLOTS], [0; SLOTS]);
100    let mut native_state: State = ([0; SLOTS], [0; SLOTS]);
101    let script = [(PUT, 3, 300), (PUT, 7, 700), (GET, 3, 0), (PUT, 3, 333), (GET, 3, 0), (DELETE, 7, 0), (GET, 7, 0)];
102    let mut source = Vec::new();
103    for (op, key, value) in script {
104        source.clear();
105        (circuit_state.0, circuit_state.1, op, key, value).encode(&mut source);
106        let outputs = executor.run(&source);
107        let keys: [u16; SLOTS] = std::array::from_fn(|slot| outputs[slot] as u16);
108        let values: [u16; SLOTS] = std::array::from_fn(|slot| outputs[SLOTS + slot] as u16);
109        let (found, previous) = (outputs[2 * SLOTS] == 1.0, outputs[2 * SLOTS + 1] as u16);
110        circuit_state = (keys, values);
111        let native = kv_step(native_state.0, native_state.1, op, key, value);
112        native_state = (native.0, native.1);
113        assert_eq!((circuit_state, found, previous), (native_state, native.2, native.3));
114        let name = ["get", "put", "delete"][op as usize];
115        println!("  {name:6} {key:>2} {value:>4}  ->  found {found:5}, previous {previous}");
116    }
117
118    // 100,000 independent operations on 100,000 different stores, in one GPU dispatch.
119    let mut rng = Rng(0x9E37_79B9_7F4A_7C15);
120    let records: Vec<Operation> = (0..100_000)
121        .map(|_| {
122            let keys: [u16; SLOTS] = std::array::from_fn(|_| rng.below(16) as u16);
123            let values: [u16; SLOTS] = std::array::from_fn(|_| rng.below(65_536) as u16);
124            (keys, values, rng.below(3) as u8, 1 + rng.below(15) as u16, rng.below(65_536) as u16)
125        })
126        .collect();
127    let start = Instant::now();
128    let native: Vec<_> =
129        records.iter().map(|&(keys, values, op, key, value)| kv_step(keys, values, op, key, value)).collect();
130    let native_time = start.elapsed();
131    let start = Instant::now();
132    let on_cpu = compiled.run_batch(&records).unwrap();
133    let cpu_time = start.elapsed();
134    assert_eq!(on_cpu, native, "the CPU batch executor disagrees with the function");
135    let gpu = Gpu::new().expect("a GPU adapter");
136    let batch = GpuBatch::new(&gpu, &compiled).unwrap();
137    let _ = batch.run(&gpu, &records[..1024]).unwrap();
138    let start = Instant::now();
139    let on_gpu = batch.run(&gpu, &records).unwrap();
140    let gpu_time = start.elapsed();
141    assert_eq!(on_gpu, native, "the GPU disagrees with the function");
142    println!("100,000 operations on separate stores; every result matches the function");
143    let threads = std::thread::available_parallelism().map_or(1, |count| count.get());
144    println!("plain Rust: {native_time:>9.1?}");
145    println!("CPU batch executor ({threads} threads): {cpu_time:>9.1?}  (range checks included)");
146    println!("GPU ({}, {:?}): {gpu_time:>9.1?}  (upload, run, read back)", gpu.info.name, gpu.info.device_type);
147}