Skip to content

Add affine MinHash permutation schemes; refresh benchmarks and docs#323

Merged
ekzhu merged 6 commits into
masterfrom
feat-affine
Jul 5, 2026
Merged

Add affine MinHash permutation schemes; refresh benchmarks and docs#323
ekzhu merged 6 commits into
masterfrom
feat-affine

Conversation

@ekzhu

@ekzhu ekzhu commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Introduce affine multiply-shift permutation schemes for MinHash — affine32 (the new default) and affine64 — which remove the legacy scheme's large-set similarity bias, run faster, and use less memory. A legacy mode keeps backward compatibility with sketches from earlier versions.

Also includes an optional CuPy GPU path for update_batch, and refreshed synthetic benchmarks, figures, and documentation across the MinHash, LSH, LSH Forest, and LSH Ensemble pages.

Introduce affine multiply-shift permutation schemes for MinHash --
affine32 (the new default) and affine64 -- which remove the legacy
scheme's large-set similarity bias, run faster, and use less memory,
keeping a legacy mode for backward compatibility. Also refreshes the
synthetic benchmarks, figures, and documentation across the MinHash,
LSH, LSH Forest, and LSH Ensemble pages, and adds an optional GPU
update path for update_batch.
@ekzhu ekzhu linked an issue Jul 4, 2026 that may be closed by this pull request

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request bumps the package version to 2.0.0 and introduces new affine permutation schemes ("affine32" and "affine64") alongside the "legacy" scheme to fix similarity over-estimation bias on large sets. The default scheme is now "affine32", which halves memory usage and significantly speeds up updates. The changes update serialization and deserialization formats for MinHash, LeanMinHash, and bBitMinHash, add scheme consistency checks across LSH indexes, introduce extensive benchmark scripts, update documentation, and expand test coverage. Since there are no review comments, I have no feedback to provide.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

ekzhu added 4 commits July 4, 2026 00:00
- Add test_estimation_accuracy_per_scheme: b-bit Jaccard estimates stay
  within tolerance of the true similarity under affine32/affine64/legacy
  (defense-in-depth, since b-bit keeps the softer low bits of the
  multiply-shift output).
- Note that the LSH Ensemble permutation-scheme guard lives in the inner
  MinHashLSH (_query_b), so it is checked lazily and per non-empty
  partition; a query reaching only empty partitions returns nothing.
- Note that the affine estimator's accuracy is empirical (a*x+b mod 2^w
  is 2-universal but not provably min-wise independent), resting on the
  bijection property, the fmix pre-mix, and the bias benchmarks.
- test_minhash_gpu.py: add TestGPUPathOnCPU, which injects a
  numpy-backed CuPy stand-in (and forces the availability cache) so the
  GPU branch of update_batch executes on CPU and is asserted equal to
  the CPU path for all three schemes. Includes a mutation guard --
  wrong-dtype (uint64) affine32 caches change the pre-truncation min
  ordering and are caught. This always runs, unlike the hardware-gated
  parity tests.
- test_minhash_schemes.py: assert LeanMinHash and bBitMinHash
  __setstate__ reject a corrupt scheme-code byte (the pickle path,
  distinct from the already-tested deserialize path); add count()
  cardinality accuracy per scheme on structured input -- affine stays
  within tolerance, legacy collapses -- the exact bias class the affine
  schemes fix.
pybloomfilter is an optional dependency (the bloom extra); the tests
errored with ImportError when it was absent instead of skipping. Guard
both test classes with skipUnless on an availability flag, matching how
the GPU and storage-backend tests handle their optional dependencies.
Tests still run and pass when the bloom extra is installed.
The pybloomfilter skip guard keeps local runs green without the optional
bloom extra, but CI installs it (.github/workflows/test.yml), so a
missing pybloomfilter under CI is a broken setup, not an absent optional
dependency. Re-raise the ImportError when the CI env var is set, so the
bloom tests fail loudly rather than silently skipping and dropping
coverage.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces new MinHash permutation schemes (“affine32” default, plus “affine64” and backward-compatible “legacy”), adds scheme-consistency guards across MinHash-based indexes/serializations, and refreshes related tests, benchmarks, figures, and documentation for the 2.0.0 release.

Changes:

  • Add affine multiply-shift permutation schemes (with fmix pre-mix), make affine32 the default, and preserve bit-exact compatibility via scheme="legacy".
  • Enforce scheme consistency across MinHash comparisons and LSH-family indexes; extend LeanMinHash and b-bit MinHash serialization formats to encode scheme (while keeping legacy bytes unchanged).
  • Refresh/expand tests, GPU-path coverage, benchmarks, and docs to reflect new schemes and performance/accuracy claims.

Reviewed changes

Copilot reviewed 37 out of 64 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
uv.lock Updates locked project version to 2.0.0.
pyproject.toml Bumps package version to 2.0.0.
README.rst Adds upgrade note about the new default MinHash scheme and migration guidance.
docs/minhash.rst Documents permutation schemes, migration, and updated benchmark/GPU guidance.
docs/lsh.rst Rewrites/expands MinHash LSH documentation and adds new benchmark discussion.
docs/lshforest.rst Rewrites/expands LSH Forest documentation and adds benchmark/characteristics sections.
docs/lshensemble.rst Reworks LSH Ensemble benchmark explanation and figures narrative.
docs/documentation.rst Adds API-doc entry for b-bit MinHash.
docs/b-bit-minhash.rst Adds new b-bit MinHash documentation page.
docs/_static/minhash/affine_scheme.svg Adds new diagram illustrating the affine scheme pipeline.
datasketch/minhash.py Implements schemes, fmix pre-mix, scheme guards, GPU branching per scheme, and legacy compatibility behavior.
datasketch/lsh.py Learns/enforces MinHash scheme per index and prevents mixed-scheme merges/queries.
datasketch/lshforest.py Learns/enforces scheme on add/query; updates hashvalue reconstruction to support 32/64-bit storage.
datasketch/lshensemble.py Clarifies where scheme checks occur in the ensemble query path.
datasketch/lsh_bloom.py Adds scheme learning/guarding and makes band hashing deterministic across dtypes.
datasketch/lean_minhash.py Adds scheme-aware construction, scheme-tagged serialization for affine schemes, and legacy byte compatibility.
datasketch/b_bit_minhash.py Adds scheme tracking/guards and scheme-tagged pickle payloads for non-legacy schemes.
datasketch/aio/lsh.py Adds scheme learning/guarding in async LSH inserts/queries.
datasketch/init.py Exposes sha1_hash64 at the package top level.
test/test_minhash.py Updates byte-token expectations for the new default scheme and adds affine64 coverage.
test/test_minhash_schemes.py Adds comprehensive scheme, serialization, compatibility, and statistical-behavior tests.
test/test_minhash_gpu.py Expands GPU tests across schemes and adds a CuPy shim to exercise GPU branch on CPU.
test/test_lshforest.py Updates reconstruction test to pass scheme explicitly when rebuilding from raw hashvalues.
test/test_lshbloom.py Skips bloom tests when optional dep missing (but fails in CI) and adds scheme-mismatch tests.
test/test_lean_minhash.py Requires scheme when constructing from raw hashvalues and adds affine bytesize assertions.
benchmark/sketches/similarity_benchmark.py Ports benchmark off Python2/pyhash to Python3/mmh3 seeded hash function.
benchmark/sketches/inclusion_benchmark.py Ports benchmark off Python2/pyhash to Python3/mmh3 seeded hash function.
benchmark/sketches/cardinality_benchmark.py Ports benchmark off Python2/pyhash to Python3/mmh3 seeded hash function.
benchmark/sketches/minhash_benchmark.py Refreshes benchmark to compare multiple hash functions and output updated figure.
benchmark/sketches/minhash_scheme_bias_benchmark.py Adds new large-scale bias experiment reproducer and plotting utility.
benchmark/sketches/b_bit_minhash_benchmark.py Reworks b-bit benchmark to focus on estimator spread vs exact similarity.
benchmark/indexes/synthetic_data.py Adds shared synthetic corpora + exact metrics helpers for index benchmarks.
benchmark/indexes/lsh_synthetic_benchmark.py Adds synthetic accuracy/speed benchmark for MinHashLSH.
benchmark/indexes/lsh_similarity_benchmark.py Adds planted-neighbor retrieval-characteristics benchmark for MinHashLSH.
benchmark/indexes/lshforest_synthetic_benchmark.py Adds synthetic top-k benchmark for MinHashLSHForest.
benchmark/indexes/lshforest_similarity_benchmark.py Adds retrieval-characteristics benchmark for MinHashLSHForest.
benchmark/indexes/lshensemble_synthetic_benchmark.py Adds synthetic partitioning-effect benchmark for MinHashLSHEnsemble.
benchmark/indexes/lsh_20newsgroups_benchmark.py Adds 20 newsgroups benchmark recreating the docs LSH figure.
benchmark/indexes/containment/lshensemble_opendata_benchmark.py Adds (legacy-style) open-data benchmark script under containment benchmarks.
Comments suppressed due to low confidence (1)

datasketch/b_bit_minhash.py:98

  • This ValueError message uses a backslash line-continuation inside the string literal, which includes indentation spaces in the actual error text. Using a single-line message avoids surprising whitespace in user-facing errors.
        if self.seed != other.seed:
            raise ValueError(
                "Cannot compare two b-bit MinHashes with different\
                    set of permutations"
            )

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread datasketch/minhash.py
Comment thread datasketch/minhash.py
Comment thread datasketch/b_bit_minhash.py
Comment thread datasketch/b_bit_minhash.py
Comment thread datasketch/lshforest.py Outdated
Collapse backslash line-continuations inside ValueError message
literals (13 sites in minhash.py, lean_minhash.py, b_bit_minhash.py) --
they embedded the source indentation into user-facing error text.

Validate segment bytes in MinHashLSHForest.get_minhash_hashvalues
before inferring the value width: the segment length must be a whole
number of 4- or 8-byte values and all segments must agree, so corrupted
or truncated storage raises ValueError instead of being silently
reinterpreted with the wrong dtype. Add a corrupt-segment test.
@ekzhu ekzhu merged commit 24e27cd into master Jul 5, 2026
25 checks passed
@ekzhu ekzhu deleted the feat-affine branch July 5, 2026 06:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Choice of np.uint64?

2 participants