Skip to main content

flash_map/
lib.rs

1//! FlashMap — GPU-native concurrent hash map.
2//!
3//! Bulk-only API designed for maximum GPU throughput:
4//! - `bulk_get`: parallel key lookup
5//! - `bulk_insert`: parallel key-value insertion (updates existing keys)
6//! - `bulk_remove`: parallel key removal (tombstone-based)
7//!
8//! SoA (Struct of Arrays) memory layout on GPU for coalesced access.
9//! Linear probing with identity hash (zero compute for pre-hashed keys).
10//!
11//! # Features
12//!
13//! - `cuda` — GPU backend via CUDA (requires NVIDIA GPU + CUDA toolkit)
14//! - `cpu-fallback` — CPU backend for development/testing (default)
15//!
16//! # Example
17//!
18//! ```rust,no_run
19//! use flash_map::{FlashMap, HashStrategy};
20//!
21//! let mut map: FlashMap<[u8; 32], [u8; 128]> =
22//!     FlashMap::with_capacity(1_000_000).unwrap();
23//!
24//! // Insert 1M key-value pairs in one GPU kernel launch
25//! let pairs: Vec<([u8; 32], [u8; 128])> = generate_pairs();
26//! map.bulk_insert(&pairs).unwrap();
27//!
28//! // Lookup
29//! let keys: Vec<[u8; 32]> = pairs.iter().map(|(k, _)| *k).collect();
30//! let results: Vec<Option<[u8; 128]>> = map.bulk_get(&keys).unwrap();
31//! # fn generate_pairs() -> Vec<([u8; 32], [u8; 128])> { vec![] }
32//! ```
33
34#[cfg(not(any(feature = "cuda", feature = "rayon", feature = "cpu-fallback")))]
35compile_error!(
36    "flash-map: enable at least one of 'cuda', 'rayon', or 'cpu-fallback' features"
37);
38
39mod error;
40mod hash;
41
42#[cfg(feature = "cuda")]
43mod gpu;
44
45#[cfg(all(feature = "cpu-fallback", not(feature = "rayon")))]
46mod cpu;
47
48#[cfg(feature = "rayon")]
49mod rayon_cpu;
50
51#[cfg(feature = "tokio")]
52mod async_map;
53
54pub use bytemuck::Pod;
55pub use error::FlashMapError;
56pub use hash::HashStrategy;
57
58#[cfg(feature = "tokio")]
59pub use async_map::AsyncFlashMap;
60
61use bytemuck::Pod as PodBound;
62
63// ---------------------------------------------------------------------------
64// FlashMap — public API
65// ---------------------------------------------------------------------------
66
67/// GPU-native concurrent hash map with bulk-only operations.
68///
69/// Generic over fixed-size key `K` and value `V` types that implement
70/// [`bytemuck::Pod`] (plain old data — `Copy`, fixed layout, any bit
71/// pattern valid).
72///
73/// Common type combinations:
74/// - `FlashMap<[u8; 32], [u8; 128]>` — blockchain state (pubkey → account)
75/// - `FlashMap<u64, u64>` — numeric keys and values
76/// - `FlashMap<[u8; 32], [u8; 32]>` — hash → hash mappings
77pub struct FlashMap<K: PodBound, V: PodBound> {
78    inner: FlashMapBackend<K, V>,
79}
80
81enum FlashMapBackend<K: PodBound, V: PodBound> {
82    #[cfg(feature = "cuda")]
83    Gpu(gpu::GpuFlashMap<K, V>),
84    #[cfg(feature = "rayon")]
85    Rayon(rayon_cpu::RayonFlashMap<K, V>),
86    #[cfg(all(feature = "cpu-fallback", not(feature = "rayon")))]
87    Cpu(cpu::CpuFlashMap<K, V>),
88}
89
90impl<K: PodBound + Send + Sync, V: PodBound + Send + Sync> FlashMap<K, V> {
91    /// Create a FlashMap with the given capacity using default settings.
92    ///
93    /// Tries GPU first (if `cuda` feature enabled), falls back to CPU.
94    /// Capacity is rounded up to the next power of 2.
95    pub fn with_capacity(capacity: usize) -> Result<Self, FlashMapError> {
96        FlashMapBuilder::new(capacity).build()
97    }
98
99    /// Create a builder for fine-grained configuration.
100    pub fn builder(capacity: usize) -> FlashMapBuilder {
101        FlashMapBuilder::new(capacity)
102    }
103
104    /// Look up multiple keys in parallel. Returns `None` for missing keys.
105    pub fn bulk_get(&self, keys: &[K]) -> Result<Vec<Option<V>>, FlashMapError> {
106        match &self.inner {
107            #[cfg(feature = "cuda")]
108            FlashMapBackend::Gpu(m) => m.bulk_get(keys),
109            #[cfg(feature = "rayon")]
110            FlashMapBackend::Rayon(m) => m.bulk_get(keys),
111            #[cfg(all(feature = "cpu-fallback", not(feature = "rayon")))]
112            FlashMapBackend::Cpu(m) => m.bulk_get(keys),
113        }
114    }
115
116    /// Insert multiple key-value pairs in parallel.
117    ///
118    /// Returns the number of **new** insertions (updates don't count).
119    /// If a key already exists, its value is updated in place.
120    ///
121    /// # Invariant
122    ///
123    /// No duplicate keys within a single batch. If the same key appears
124    /// multiple times, behavior is undefined (one will win, but which
125    /// one is non-deterministic on GPU).
126    pub fn bulk_insert(&mut self, pairs: &[(K, V)]) -> Result<usize, FlashMapError> {
127        match &mut self.inner {
128            #[cfg(feature = "cuda")]
129            FlashMapBackend::Gpu(m) => m.bulk_insert(pairs),
130            #[cfg(feature = "rayon")]
131            FlashMapBackend::Rayon(m) => m.bulk_insert(pairs),
132            #[cfg(all(feature = "cpu-fallback", not(feature = "rayon")))]
133            FlashMapBackend::Cpu(m) => m.bulk_insert(pairs),
134        }
135    }
136
137    /// Remove multiple keys in parallel (tombstone-based).
138    ///
139    /// Returns the number of keys actually removed.
140    pub fn bulk_remove(&mut self, keys: &[K]) -> Result<usize, FlashMapError> {
141        match &mut self.inner {
142            #[cfg(feature = "cuda")]
143            FlashMapBackend::Gpu(m) => m.bulk_remove(keys),
144            #[cfg(feature = "rayon")]
145            FlashMapBackend::Rayon(m) => m.bulk_remove(keys),
146            #[cfg(all(feature = "cpu-fallback", not(feature = "rayon")))]
147            FlashMapBackend::Cpu(m) => m.bulk_remove(keys),
148        }
149    }
150
151    /// Number of occupied entries.
152    pub fn len(&self) -> usize {
153        match &self.inner {
154            #[cfg(feature = "cuda")]
155            FlashMapBackend::Gpu(m) => m.len(),
156            #[cfg(feature = "rayon")]
157            FlashMapBackend::Rayon(m) => m.len(),
158            #[cfg(all(feature = "cpu-fallback", not(feature = "rayon")))]
159            FlashMapBackend::Cpu(m) => m.len(),
160        }
161    }
162
163    /// Whether the map is empty.
164    pub fn is_empty(&self) -> bool {
165        self.len() == 0
166    }
167
168    /// Total slot capacity (always a power of 2).
169    pub fn capacity(&self) -> usize {
170        match &self.inner {
171            #[cfg(feature = "cuda")]
172            FlashMapBackend::Gpu(m) => m.capacity(),
173            #[cfg(feature = "rayon")]
174            FlashMapBackend::Rayon(m) => m.capacity(),
175            #[cfg(all(feature = "cpu-fallback", not(feature = "rayon")))]
176            FlashMapBackend::Cpu(m) => m.capacity(),
177        }
178    }
179
180    /// Current load factor (0.0 to 1.0).
181    pub fn load_factor(&self) -> f64 {
182        match &self.inner {
183            #[cfg(feature = "cuda")]
184            FlashMapBackend::Gpu(m) => m.load_factor(),
185            #[cfg(feature = "rayon")]
186            FlashMapBackend::Rayon(m) => m.load_factor(),
187            #[cfg(all(feature = "cpu-fallback", not(feature = "rayon")))]
188            FlashMapBackend::Cpu(m) => m.load_factor(),
189        }
190    }
191
192    /// Remove all entries, resetting to empty.
193    pub fn clear(&mut self) -> Result<(), FlashMapError> {
194        match &mut self.inner {
195            #[cfg(feature = "cuda")]
196            FlashMapBackend::Gpu(m) => m.clear(),
197            #[cfg(feature = "rayon")]
198            FlashMapBackend::Rayon(m) => m.clear(),
199            #[cfg(all(feature = "cpu-fallback", not(feature = "rayon")))]
200            FlashMapBackend::Cpu(m) => m.clear(),
201        }
202    }
203}
204
205impl<K: PodBound + Send + Sync, V: PodBound + Send + Sync> std::fmt::Debug
206    for FlashMap<K, V>
207{
208    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209        let backend = match &self.inner {
210            #[cfg(feature = "cuda")]
211            FlashMapBackend::Gpu(_) => "GPU",
212            #[cfg(feature = "rayon")]
213            FlashMapBackend::Rayon(_) => "Rayon",
214            #[cfg(all(feature = "cpu-fallback", not(feature = "rayon")))]
215            FlashMapBackend::Cpu(_) => "CPU",
216        };
217        f.debug_struct("FlashMap")
218            .field("backend", &backend)
219            .field("len", &self.len())
220            .field("capacity", &self.capacity())
221            .field("load_factor", &format!("{:.1}%", self.load_factor() * 100.0))
222            .finish()
223    }
224}
225
226// ---------------------------------------------------------------------------
227// Builder
228// ---------------------------------------------------------------------------
229
230/// Builder for configuring a [`FlashMap`].
231pub struct FlashMapBuilder {
232    capacity: usize,
233    hash_strategy: HashStrategy,
234    device_id: usize,
235    force_cpu: bool,
236}
237
238impl FlashMapBuilder {
239    /// Create a builder targeting the given capacity.
240    pub fn new(capacity: usize) -> Self {
241        Self {
242            capacity,
243            hash_strategy: HashStrategy::Identity,
244            device_id: 0,
245            force_cpu: false,
246        }
247    }
248
249    /// Set the hash strategy (default: Identity).
250    pub fn hash_strategy(mut self, strategy: HashStrategy) -> Self {
251        self.hash_strategy = strategy;
252        self
253    }
254
255    /// Set the CUDA device ordinal (default: 0).
256    pub fn device_id(mut self, id: usize) -> Self {
257        self.device_id = id;
258        self
259    }
260
261    /// Force CPU backend even if CUDA is available.
262    pub fn force_cpu(mut self) -> Self {
263        self.force_cpu = true;
264        self
265    }
266
267    /// Build the FlashMap. Tries GPU first, falls back to CPU if available.
268    pub fn build<K: PodBound + Send + Sync, V: PodBound + Send + Sync>(
269        self,
270    ) -> Result<FlashMap<K, V>, FlashMapError> {
271        let mut _gpu_err: Option<FlashMapError> = None;
272
273        #[cfg(feature = "cuda")]
274        if !self.force_cpu {
275            match gpu::GpuFlashMap::<K, V>::new(
276                self.capacity,
277                self.hash_strategy,
278                self.device_id,
279            ) {
280                Ok(m) => return Ok(FlashMap { inner: FlashMapBackend::Gpu(m) }),
281                Err(e) => _gpu_err = Some(e),
282            }
283        }
284
285        #[cfg(feature = "rayon")]
286        {
287            if let Some(ref e) = _gpu_err {
288                eprintln!("[flash-map] GPU unavailable ({e}), using Rayon backend");
289            }
290            return Ok(FlashMap {
291                inner: FlashMapBackend::Rayon(rayon_cpu::RayonFlashMap::new(
292                    self.capacity,
293                    self.hash_strategy,
294                )),
295            });
296        }
297
298        #[cfg(all(feature = "cpu-fallback", not(feature = "rayon")))]
299        {
300            if let Some(ref e) = _gpu_err {
301                eprintln!("[flash-map] GPU unavailable ({e}), using CPU fallback");
302            }
303            return Ok(FlashMap {
304                inner: FlashMapBackend::Cpu(cpu::CpuFlashMap::new(
305                    self.capacity,
306                    self.hash_strategy,
307                )),
308            });
309        }
310
311        #[allow(unreachable_code)]
312        match _gpu_err {
313            Some(e) => Err(e),
314            None => Err(FlashMapError::NoBackend),
315        }
316    }
317}