Skip to main content

zer/
lib.rs

1//! `zernified entity resolution library.
2//!
3//! Provides [`Comparator`], [`Scorer`], and a [`Backend`] abstraction that
4//! selects GPU acceleration automatically when compiled with the `cuda` or
5//! `vulkan` features and suitable hardware is present.  Without those features
6//! the crate compiles and runs entirely on CPU via `zer-compare`.
7//!
8//! # Quick start
9//!
10//! ```rust,no_run
11//! use zer::prelude::*;
12//!
13//! let schema = SchemaBuilder::new()
14//!     .field("naam",  FieldKind::Name)
15//!     .field("datum", FieldKind::Date)
16//!     .build().unwrap();
17//!
18//! let backend    = Backend::auto_detect();        // CUDA → Vulkan → AVX2 → CPU
19//! let comparator = Comparator::new(&schema, &backend);
20//! let scorer     = Scorer::new(&backend);
21//! ```
22//!
23//! # Feature flags
24//!
25//! **Compute backends** (mutually exclusive in practice; pick one):
26//!
27//! | Flag             | Description                                                              |
28//! |------------------|--------------------------------------------------------------------------|
29//! | `cuda`           | NVIDIA CUDA via `zer-compute`, requires CUDA Toolkit 13.1+ and `nvcc`   |
30//! | `vulkan`         | Vulkan 1.3 compute via `zer-compute`, requires `slangc` on `PATH`        |
31//! | `avx2`           | x86_64 AVX2 SIMD via `zer-compute`, no external toolchain required       |
32//! | `cpu`            | Explicit scalar CPU path via `zer-compute` (Rayon parallel)              |
33//! | `debug-shaders`  | Embed debug info in CUDA kernels for `cuda-gdb` / Nsight (needs `cuda`) |
34//!
35//! **Pipeline integration:**
36//!
37//! | Flag       | Description                                                              |
38//! |------------|--------------------------------------------------------------------------|
39//! | `pipeline` | Enable `Pipeline`, `Ingester`, and related types from `zer-pipeline`     |
40//!
41//! **Neural judge ORT execution providers** (independent of compute backend):
42//!
43//! | Flag             | Description                                                              |
44//! |------------------|--------------------------------------------------------------------------|
45//! | `judge_cpu`      | Scalar CPU execution provider for ORT (no extra dependencies)            |
46//! | `judge_cuda`     | NVIDIA CUDA execution provider for ORT                                   |
47//! | `judge_rocm`     | AMD ROCm execution provider for ORT                                      |
48//! | `judge_directml` | Windows DirectML execution provider for ORT                              |
49//! | `judge_openvino` | Intel OpenVINO execution provider for ORT                                |
50//!
51//! # CPU-only usage
52//!
53//! Users who never need GPU can depend on `zer-compare` directly and never
54//! import this crate.  `zer_compare::FieldComparator` and
55//! `zer_compare::FellegiSunterScorer` are the raw CPU implementations.
56
57#[cfg(any(feature = "cuda", feature = "avx2", feature = "vulkan"))]
58use std::sync::Arc;
59
60use zer_core::{
61    comparison::{ComparisonBatch, ComparisonVector},
62    record::Record,
63    record_pool::RecordPool,
64    schema::Schema,
65    scoring::{ModelParams, ScoredPair},
66    traits::{Comparator as ComparatorTrait, Result as ZerResult, Scorer as ScorerTrait},
67};
68
69// ── Backend ───────────────────────────────────────────────────────────────────
70
71enum BackendInner {
72    Cpu,
73    #[cfg(any(feature = "cuda", feature = "avx2", feature = "vulkan"))]
74    Gpu(Arc<zer_compute::DeviceBackend>),
75}
76
77/// Opaque compute backend handle.
78///
79/// Create once and share between [`Comparator`] and [`Scorer`] so both use the
80/// same underlying GPU device.
81///
82/// ```rust,no_run
83/// use zer::prelude::*;
84///
85/// let schema     = SchemaBuilder::new().field("naam", FieldKind::Name).build().unwrap();
86/// let backend    = Backend::auto_detect();
87/// let comparator = Comparator::new(&schema, &backend);
88/// let scorer     = Scorer::new(&backend);
89/// ```
90pub struct Backend {
91    inner: BackendInner,
92    name: &'static str,
93}
94
95impl Backend {
96    /// Read `--target=<name>` from process args and return the matching backend.
97    ///
98    /// Falls back to CPU when the flag is absent, no hardware probing.
99    /// Pass `--target=auto` to restore the hardware-detection order
100    /// (CUDA → Vulkan → AVX2 → CPU).
101    pub fn auto_detect() -> Self {
102        match std::env::args()
103            .find_map(|a| a.strip_prefix("--target=").map(str::to_owned))
104            .as_deref()
105        {
106            Some(t) => Self::from_target(t),
107            None => Self::cpu(),
108        }
109    }
110
111    /// Force the CPU backend regardless of available hardware.
112    pub fn cpu() -> Self {
113        Self {
114            inner: BackendInner::Cpu,
115            name: "cpu",
116        }
117    }
118
119    /// Select a backend by name, called by `auto_detect()` to resolve `--target=<name>`.
120    ///
121    /// Accepted values: `"auto"` (hardware-detect), `"cpu"`, `"cuda"`, `"avx2"`, `"vulkan"`.
122    ///
123    /// Exits with a diagnostic if the target is unknown, not compiled in, or hardware init fails.
124    pub fn from_target(target: &str) -> Self {
125        if target == "cpu" {
126            return Self::cpu();
127        }
128
129        #[cfg(any(feature = "cuda", feature = "avx2", feature = "vulkan"))]
130        {
131            let pref = match target {
132                "auto" => zer_compute::BackendPreference::Auto,
133                "cuda" => zer_compute::BackendPreference::Cuda,
134                "vulkan" => zer_compute::BackendPreference::Vulkan,
135                "avx2" => zer_compute::BackendPreference::Avx2,
136                other => {
137                    tracing::error!(
138                        target = other,
139                        "unknown --target; valid: auto, cpu, avx2, cuda, vulkan"
140                    );
141                    std::process::exit(1);
142                }
143            };
144            return match zer_compute::DeviceBackend::from_preference(pref) {
145                Ok(dev) => {
146                    let name = dev.name();
147                    if dev.is_accelerated() {
148                        Self {
149                            inner: BackendInner::Gpu(Arc::new(dev)),
150                            name,
151                        }
152                    } else {
153                        Self {
154                            inner: BackendInner::Cpu,
155                            name: "cpu",
156                        }
157                    }
158                }
159                Err(e) => {
160                    tracing::error!(target, error = %e, "--target unavailable");
161                    std::process::exit(1);
162                }
163            };
164        }
165
166        #[allow(unreachable_code)]
167        {
168            if target == "auto" {
169                return Self::cpu();
170            }
171            tracing::error!(
172                target,
173                "unknown --target; valid values when built without GPU features: auto, cpu"
174            );
175            std::process::exit(1);
176        }
177    }
178
179    /// Human-readable name of the active backend: `"cpu"`, `"cuda"`, or `"avx2"`.
180    pub fn name(&self) -> &'static str {
181        self.name
182    }
183
184    /// `true` when a GPU backend is active.
185    pub fn is_gpu(&self) -> bool {
186        !matches!(self.inner, BackendInner::Cpu)
187    }
188}
189
190// ── Comparator ────────────────────────────────────────────────────────────────
191
192enum ComparatorInner {
193    Cpu(zer_compare::FieldComparator),
194    #[cfg(any(feature = "cuda", feature = "avx2", feature = "vulkan"))]
195    Gpu(zer_compute::DeviceComparator),
196}
197
198/// Pairwise record comparator with automatic GPU/CPU selection.
199///
200/// Wraps `FieldComparator` (CPU) or `DeviceComparator` (GPU) depending on the
201/// [`Backend`].  Implements [`ComparatorTrait`] identically in both cases.
202pub struct Comparator {
203    inner: ComparatorInner,
204}
205
206impl Comparator {
207    /// Wrap an already-constructed [`zer_compare::FieldComparator`] directly.
208    ///
209    /// Use this when you want to override default similarity functions via
210    /// [`zer_compare::FieldComparator::with_fns`] before creating the comparator.
211    /// Always uses the CPU path; GPU acceleration is not available this way.
212    pub fn from_cpu(fc: zer_compare::FieldComparator) -> Self {
213        Self {
214            inner: ComparatorInner::Cpu(fc),
215        }
216    }
217
218    /// Build a comparator from a schema and backend.
219    pub fn new(schema: &Schema, backend: &Backend) -> Self {
220        match &backend.inner {
221            BackendInner::Cpu => Self {
222                inner: ComparatorInner::Cpu(zer_compare::FieldComparator::from_schema(schema)),
223            },
224            #[cfg(any(feature = "cuda", feature = "avx2", feature = "vulkan"))]
225            BackendInner::Gpu(dev) => Self {
226                inner: ComparatorInner::Gpu(
227                    zer_compute::DeviceComparator::new(Arc::clone(dev), schema).unwrap(),
228                ),
229            },
230        }
231    }
232
233    /// Name of the active backend, for diagnostics.
234    pub fn backend_name(&self) -> &'static str {
235        match &self.inner {
236            ComparatorInner::Cpu(_) => "cpu",
237            #[cfg(any(feature = "cuda", feature = "avx2", feature = "vulkan"))]
238            ComparatorInner::Gpu(c) => c.backend_name(),
239        }
240    }
241
242    /// Primary hot-path: pool-native batch comparison.
243    ///
244    /// `pool` is a `RecordPool` built from the candidate records; `pair_indices`
245    /// holds `(i, j)` pairs where `i` and `j` are indices into the pool.
246    /// Avoids all `Record::clone()` and `HashMap` lookups, the fastest path for
247    /// large BRP-style jobs where records are already loaded into a pool.
248    pub fn compare_batch_from_pool(
249        &self,
250        pool: &RecordPool,
251        pair_indices: &[(usize, usize)],
252        schema: &Schema,
253    ) -> ComparisonBatch {
254        match &self.inner {
255            ComparatorInner::Cpu(c) => c.compare_batch_from_pool(pool, pair_indices, schema),
256            #[cfg(any(feature = "cuda", feature = "avx2", feature = "vulkan"))]
257            ComparatorInner::Gpu(c) => c.compare_batch_from_pool(pool, pair_indices, schema),
258        }
259    }
260
261    /// Convenience wrapper: builds a pool from a flat `records` slice and compares
262    /// the `pair_indices` pairs.  No `Record::clone()`.
263    pub fn compare_batch_indexed(
264        &self,
265        records: &[Record],
266        pair_indices: &[(usize, usize)],
267        schema: &Schema,
268    ) -> ComparisonBatch {
269        let pool = RecordPool::from_records(records, schema);
270        self.compare_batch_from_pool(&pool, pair_indices, schema)
271    }
272}
273
274impl ComparatorTrait for Comparator {
275    fn compare(&self, a: &Record, b: &Record, schema: &Schema) -> ComparisonVector {
276        match &self.inner {
277            ComparatorInner::Cpu(c) => c.compare(a, b, schema),
278            #[cfg(any(feature = "cuda", feature = "avx2", feature = "vulkan"))]
279            ComparatorInner::Gpu(c) => c.compare(a, b, schema),
280        }
281    }
282
283    fn compare_batch_from_pool(
284        &self,
285        pool: &RecordPool,
286        indices: &[(usize, usize)],
287        schema: &Schema,
288    ) -> ComparisonBatch {
289        self.compare_batch_from_pool(pool, indices, schema)
290    }
291}
292
293// ── Scorer ────────────────────────────────────────────────────────────────────
294
295enum ScorerInner {
296    Cpu(zer_compare::FellegiSunterScorer),
297    #[cfg(any(feature = "cuda", feature = "avx2", feature = "vulkan"))]
298    Gpu(zer_compute::DeviceScorer),
299}
300
301/// Fellegi-Sunter scorer with automatic GPU/CPU EM acceleration.
302///
303/// `score` / `score_batch` always run on CPU, no kernel overhead for small
304/// operations.  `estimate_params` uses the GPU EM kernel when the backend is
305/// GPU and the batch exceeds the transfer break-even threshold; otherwise it
306/// falls back to `zer_compare::run_em` on the CPU.
307pub struct Scorer {
308    inner: ScorerInner,
309}
310
311impl Scorer {
312    /// Build a scorer using the given backend.
313    pub fn new(backend: &Backend) -> Self {
314        match &backend.inner {
315            BackendInner::Cpu => Self {
316                inner: ScorerInner::Cpu(zer_compare::FellegiSunterScorer),
317            },
318            #[cfg(any(feature = "cuda", feature = "avx2", feature = "vulkan"))]
319            BackendInner::Gpu(dev) => Self {
320                inner: ScorerInner::Gpu(zer_compute::DeviceScorer::new(Arc::clone(dev))),
321            },
322        }
323    }
324
325    /// Name of the active backend, for diagnostics.
326    pub fn backend_name(&self) -> &'static str {
327        match &self.inner {
328            ScorerInner::Cpu(_) => "cpu",
329            #[cfg(any(feature = "cuda", feature = "avx2", feature = "vulkan"))]
330            ScorerInner::Gpu(s) => s.backend_name(),
331        }
332    }
333}
334
335impl ScorerTrait for Scorer {
336    fn score(&self, vector: &ComparisonVector, params: &ModelParams) -> ScoredPair {
337        match &self.inner {
338            ScorerInner::Cpu(s) => s.score(vector, params),
339            #[cfg(any(feature = "cuda", feature = "avx2", feature = "vulkan"))]
340            ScorerInner::Gpu(s) => s.score(vector, params),
341        }
342    }
343
344    fn score_batch(&self, batch: &ComparisonBatch, params: &ModelParams) -> Vec<ScoredPair> {
345        match &self.inner {
346            ScorerInner::Cpu(s) => s.score_batch(batch, params),
347            #[cfg(any(feature = "cuda", feature = "avx2", feature = "vulkan"))]
348            ScorerInner::Gpu(s) => s.score_batch(batch, params),
349        }
350    }
351
352    fn estimate_params(
353        &self,
354        batch: &ComparisonBatch,
355        init: Option<ModelParams>,
356        max_iter: usize,
357    ) -> ZerResult<ModelParams> {
358        match &self.inner {
359            ScorerInner::Cpu(s) => s.estimate_params(batch, init, max_iter),
360            #[cfg(any(feature = "cuda", feature = "avx2", feature = "vulkan"))]
361            ScorerInner::Gpu(s) => s.estimate_params(batch, init, max_iter),
362        }
363    }
364}
365
366// ── Low-level kernel access for power users ───────────────────────────────────
367
368/// Raw GPU kernel dispatch, for users writing custom kernels.
369///
370/// Requires the `cuda` or `avx2` feature.  Most users should use
371/// [`Comparator`] and [`Scorer`] instead.
372///
373/// # Writing a custom kernel
374///
375/// 1. Define a zero-sized marker struct and `impl Kernel for It`.
376/// 2. `impl KernelDispatch<It> for zer_compute::backend::cpu::CpuDevice`, CPU fallback.
377/// 3. `impl KernelDispatch<It> for zer_compute::backend::cuda::CudaDevice`, CUDA path.
378/// 4. Add the `impl KernelDispatch<It> for DeviceBackend` match in
379///    `zer_compute::backend::mod`.
380/// 5. Access the raw device via `zer::compute::DeviceBackend`.
381#[cfg(any(feature = "cuda", feature = "avx2", feature = "vulkan"))]
382pub mod kernel {
383    pub use zer_compute::{
384        backend::DeviceBackend,
385        error::GpuError,
386        kernel::{Kernel, KernelDispatch},
387    };
388}
389
390// ── Crate re-exports ──────────────────────────────────────────────────────────
391
392pub use zer_blocking as blocking;
393pub use zer_cluster as cluster;
394pub use zer_compare as compare;
395pub use zer_core as core;
396pub use zer_schema as schema;
397
398#[cfg(feature = "pipeline")]
399pub use zer_pipeline as pipeline;
400
401#[cfg(any(feature = "cuda", feature = "avx2", feature = "vulkan"))]
402pub use zer_compute as compute;
403
404// ── Prelude ───────────────────────────────────────────────────────────────────
405
406pub mod prelude {
407    // Concrete auto-detecting types, primary user-facing API
408    pub use crate::{Backend, Comparator, Scorer};
409
410    // Core data types
411    pub use zer_core::{
412        comparison::{ComparisonBatch, ComparisonLevel, ComparisonVector},
413        entity::{Entity, EntityId, EntityMember, ResolutionMethod},
414        error::ZerError,
415        record::{FieldValue, Record, RecordId},
416        record_pool::RecordPool,
417        schema::{FieldKind, Schema, SchemaBuilder},
418        scoring::{MatchBand, ModelParams, ScoredPair},
419        traits::{
420            BlockIndex,
421            Blocker,
422            Clusterer,
423            // Renamed to avoid shadowing the concrete Comparator / Scorer structs above
424            Comparator as ComparatorTrait,
425            EntityStore,
426            Judge,
427            JudgeVerdict,
428            RecordStore,
429            Scorer as ScorerTrait,
430        },
431        VecRecordStore,
432    };
433
434    // Blocking
435    pub use zer_blocking::{
436        keys::{
437            AddressInitialKey, AliasPhoneticKey, CameraTimeWindowKey, DateFragmentKey,
438            DateGranularity, DocumentDigitSuffixKey, DocumentSuffixKey, ExactFieldKey,
439            FuzzyYearKey, GeoGridKey, LicensePlateNormKey, PhoneticAlgo, PhoneticNameDobKey,
440            PlateOCRFuzzyKey, SuffixKey, TransliteratedPhoneticKey,
441        },
442        BlockerFactory, CompositeBlocker, InvertedIndex, SchemaCategory,
443    };
444
445    // CPU implementations, available directly for users who want the raw types
446    pub use zer_compare::{
447        AddressTokenOverlap, FellegiSunterScorer, FieldComparator, JaroWinklerSimilarity,
448        LevelThresholds, PhoneticEqualitySimilarity, SimilarityFn, StreetNumberEditDistance,
449        TokenOverlapSimilarity,
450    };
451
452    // Schema registry and artifact management (Phase 6)
453    pub use zer_schema::{
454        ModelArtifact, SchemaFingerprint, SchemaInferrer, SchemaRegistry, StartupMode,
455    };
456
457    // Clustering and entity store (Phase 6)
458    pub use zer_cluster::{ClusterConfig, ConnectedComponentsClusterer, ZalEntityStore};
459
460    // Pipeline types, available with the `pipeline` feature (no polars required)
461    #[cfg(feature = "pipeline")]
462    pub use zer_pipeline::{
463        BatchReport, ClusterIter, ClusterView, IngestResult, Ingester, Pipeline, PipelineBuilder,
464        PipelineConfig, RateConfig,
465    };
466}