Curate TextProcess DataDeduplication

Fuzzy Duplicate Removal

View as Markdown

Find and remove near-duplicate documents with small edits or reformatting using MinHash and Locality Sensitive Hashing (LSH). This approach identifies candidate pairs with a similarity threshold efficiently at scale on GPU.

For other approaches, refer to Deduplication .

How It Works

Fuzzy deduplication uses MinHash and LSH to find near-duplicate content:

  1. Computes MinHash signatures over character n-grams
  2. Uses Locality Sensitive Hashing (LSH) to find candidate matches
  3. Builds a graph of duplicate relationships
  4. Identifies groups of near-duplicate documents

Ideal for detecting documents with minor differences such as formatting changes, typos, or small edits, where documents share a high degree of overlapping content.

Before You Start

Prerequisites:

  • Ray cluster with GPU support (required for distributed processing)
  • Stable document identifiers for removal (either existing IDs or IDs generated by the workflow and removal stages)

Running in Docker: When running fuzzy deduplication inside the NeMo Curator container, ensure the container is started with --gpus all so that Ray workers can access the GPU. Without GPU access, you may see CUDARuntimeError or AttributeError: 'CUDARuntimeError' object has no attribute 'msg'. Also activate the virtual environment with source /opt/venv/env.sh after entering the container.

Quick Start

Get started with fuzzy deduplication using the following example of identifying duplicates, then remove them:

1from nemo_curator.core.client import RayClient
2from nemo_curator.stages.deduplication.fuzzy.workflow import FuzzyDeduplicationWorkflow
3from nemo_curator.stages.text.deduplication.removal_workflow import TextDuplicatesRemovalWorkflow
4
5ray_client = RayClient()
6ray_client.start()
7
8# Keep this value identical for identification and removal when using auto-generated IDs.
9input_blocksize = "1GiB"
10
11# Step 1: Identify duplicates
12fuzzy_workflow = FuzzyDeduplicationWorkflow(
13 input_path="input_data/",
14 cache_path="./cache",
15 output_path="./results",
16 text_field="text",
17 perform_removal=False,
18 input_filetype="parquet",
19 input_blocksize=input_blocksize,
20 char_ngrams=24,
21 num_bands=20,
22 minhashes_per_band=13
23)
24result = fuzzy_workflow.run()
25# result.metadata contains: total_time, num_duplicates, minhash_time, lsh_time, connected_components_pipeline_time, id_generator_path
26
27# Step 2: Remove duplicates
28removal_workflow = TextDuplicatesRemovalWorkflow(
29 input_path="input_data/",
30 ids_to_remove_path="./results/FuzzyDuplicateIds",
31 output_path="./deduplicated",
32 input_filetype="parquet",
33 input_blocksize=input_blocksize,
34 id_field="_curator_dedup_id",
35 duplicate_id_field="_curator_dedup_id",
36 id_generator_path="./results/fuzzy_id_generator.json"
37)
38result = removal_workflow.run()
39# result.metadata contains: total_time, num_duplicates_removed

Configuration

Configure fuzzy deduplication using these key parameters:

ParameterTypeDefaultDescription
input_pathstr | list[str]NonePath(s) to input files or directories
cache_pathstrRequiredDirectory to cache intermediate results
output_pathstrRequiredDirectory to write duplicate IDs and ID generator
text_fieldstr”text”Name of the text field in input data
char_ngramsint24Character n-gram size for MinHash (recommended: >= 20)
num_bandsint20Number of LSH bands (affects similarity threshold)
minhashes_per_bandint13Number of hashes per LSH band
bands_per_iterationint5Bands processed per iteration (memory tuning)
use_64_bit_hashboolFalseUse 64-bit hash (more memory, fewer collisions)
seedint42Random seed for MinHash permutations
input_filetypestr”parquet”Input file format (“parquet” or “jsonl”)
input_file_extensionslist[str] | NoneNoneExtensions to discover. None or [] uses [".parquet"] for Parquet and [".jsonl", ".json"] for JSONL. A non-empty list replaces those defaults.
input_blocksizestr | int”1GiB”Size of input blocks for processing
lsh_num_output_partitionsint | NoneNoneTotal number of partitions to write during the LSH shuffle. If None, the partition count is chosen automatically as the closest power of 2 <= the number of input tasks.
lsh_rmm_pool_sizeint | “auto” | None”auto”Size of the RMM GPU memory pool in bytes for the LSH stage. "auto" sets the pool to 90% of free GPU memory. None sets the pool to 50% of free GPU memory and allows expansion.
lsh_spill_memory_limitint | “auto” | None”auto”Device memory limit in bytes for spilling to host during the LSH stage. "auto" sets the limit to 80% of the RMM pool size. None disables spilling.
perform_removalboolFalseReserved; must remain False. Fuzzy removal is performed with TextDuplicatesRemovalWorkflow.

For recursive discovery behavior, custom suffix examples, and supported formats, see Input File Discovery.

When fuzzy duplicate IDs are auto-generated, use the same input_blocksize value for both FuzzyDeduplicationWorkflow and TextDuplicatesRemovalWorkflow. Different block sizes can change partition-to-ID mappings and cause removal to fail when resolving generated IDs.

Similarity Threshold

Control matching strictness with num_bands and minhashes_per_band:

  • More strict matching: Increase num_bands or decrease minhashes_per_band
  • Less strict matching: Decrease num_bands or increase minhashes_per_band

Default (num_bands=20, minhashes_per_band=13) provides a balanced trade-off between recall and precision for many datasets. The exact similarity at which pairs are detected depends on your data distribution.

1# Example: stricter matching (fewer pairs detected, higher required similarity)
2fuzzy_workflow = FuzzyDeduplicationWorkflow(
3 num_bands=25, # More bands = stricter matching
4 minhashes_per_band=10 # Fewer hashes per band = stricter matching
5)
6
7# Example: less strict matching (more pairs detected, lower required similarity)
8fuzzy_workflow = FuzzyDeduplicationWorkflow(
9 num_bands=15, # Fewer bands = less strict matching
10 minhashes_per_band=15 # More hashes per band = less strict matching
11)

Removing Duplicates

After identifying duplicates, use TextDuplicatesRemovalWorkflow to remove them:

1from nemo_curator.stages.text.deduplication.removal_workflow import TextDuplicatesRemovalWorkflow
2
3removal_workflow = TextDuplicatesRemovalWorkflow(
4 input_path="/path/to/input/data",
5 ids_to_remove_path="/path/to/output/FuzzyDuplicateIds",
6 output_path="/path/to/deduplicated",
7 input_filetype="parquet",
8 # Set input_file_extensions here too if identification used a custom suffix.
9 input_blocksize="1GiB", # Must match the identification workflow when IDs were auto-assigned
10 id_field="_curator_dedup_id",
11 duplicate_id_field="_curator_dedup_id",
12 id_generator_path="/path/to/output/fuzzy_id_generator.json" # Required if IDs were auto-assigned
13)
14result = removal_workflow.run()

When IDs were auto-assigned:

  • id_generator_path is required
  • Ensures consistent ID mapping between identification and removal stages

Output Format

The fuzzy deduplication process produces the following directory structure:

cache_path/
├── MinHashStage/ # MinHash signatures
│ └── *.parquet
├── LSHStage/ # LSH buckets
│ └── *.parquet
├── BucketsToEdges/ # Graph edges
│ └── *.parquet
└── ConnectedComponents/ # Connected components
└── *.parquet
output_path/
├── FuzzyDuplicateIds/ # Duplicate identification results
│ └── *.parquet # Parquet files with document IDs to remove
└── fuzzy_id_generator.json # ID generator mapping (if IDs were auto-assigned)

File Formats

The workflow produces these output files:

  1. Duplicate IDs (FuzzyDuplicateIds/*.parquet):

    • Contains document IDs to remove
    • Format: Parquet files with column: ["_curator_dedup_id"]
    • Important: Contains only the IDs of documents to remove, not the full document content
  2. ID Generator (fuzzy_id_generator.json):

    • JSON file containing ID generator state
    • Required for removal workflow when IDs were auto-assigned
    • Ensures consistent ID mapping across workflow stages
  3. Cache Files (cache_path/):

    • Intermediate results for debugging and analysis
    • Can be reused if re-running with different parameters
    • Clear cache between runs if parameters change significantly

Performance characteristics:

  • GPU-accelerated MinHash and LSH operations
  • Scales across multiple GPUs and nodes using Ray
  • bands_per_iteration controls memory usage
  • Intermediate results are cached for efficiency

GPU requirements:

  • NVIDIA GPU with CUDA support
  • Ray cluster with GPU workers

Performance tuning:

  • Memory: Adjust bands_per_iteration (lower = less memory, more iterations)
  • GPU memory (LSH): Use lsh_rmm_pool_size to control GPU memory allocation and lsh_spill_memory_limit to tune host-spilling behavior during the LSH stage. Reducing the pool size or lowering the spill threshold can prevent out-of-memory errors on smaller GPUs.
  • Shuffle partitions: Set lsh_num_output_partitions to control the number of output partitions during the LSH shuffle. More partitions reduce per-partition memory but increase I/O overhead.
  • Accuracy: Use char_ngrams &gt;= 20 to reduce false positives
  • Best practices: Clear cache between runs, use input_blocksize="1GiB"

Note: Performance depends on hardware configuration, dataset characteristics, and parameter choices such as bands_per_iteration, char_ngrams, and input_blocksize.

For comparison with other deduplication methods and guidance on when to use fuzzy deduplication, refer to the Deduplication overview .