') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); GH-45750: [C++][Python][Parquet] Implement Content-Defined Chunking for the Parquet writer by kszucs · Pull Request #45360 · apache/arrow · GitHub
Skip to content

GH-45750: [C++][Python][Parquet] Implement Content-Defined Chunking for the Parquet writer - #45360

Merged
pitrou merged 102 commits into
apache:mainfrom
kszucs:content-defined-chunking
May 13, 2025
Merged

GH-45750: [C++][Python][Parquet] Implement Content-Defined Chunking for the Parquet writer#45360
pitrou merged 102 commits into
apache:mainfrom
kszucs:content-defined-chunking

Conversation

@kszucs

@kszucskszucs commented Jan 27, 2025

Copy link
Copy Markdown
Member

Rationale for this change

I have been working on to improve Parquet's deduplication efficiency for content-addressable storages. These system generally use some kind of a CDC algorithm which are better suited for uncompressed row-major formats. Although thanks to Parquet's unique features I was able to reach good deduplication results by consistently chunking data pages by maintaining a gearhash based chunker for each column.

Deduplication efficiency

The feature enables efficient data deduplication for compressed parquet files on content addressable storage (CAS) systems such as Hugging Face Hub. There is a purpose built evaluation tool is available at https://github.com/kszucs/de used during development to continuously check the improvements and to visually inspect the results. Please take a look at the repository's readme to see how different changes made to parquet files affect the deduplication ratio when they are stored in CAS systems.

Some results calculated on all revisions of datasets.parquet

❯ de stats /tmp/datasets Writing CDC Parquet files with ZSTD compression 100%|███████████████████████████████████████████████████████████████████████████████████| 194/194 [00:12<00:00, 15.73it/s]
Writing CDC Parquet files with Snappy compression 100%|███████████████████████████████████████████████████████████████████████████████████| 194/194 [00:10<00:00, 17.95it/s]
Estimating deduplication for Parquet Estimating deduplication for CDC ZSTD Estimating deduplication for CDC Snappy ┏━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┃ ┃ ┃ Compressed Chunk ┃ ┃ Compressed Dedup ┃ Transmitted XTool ┃
┃ Title ┃ Total Size ┃ Chunk Size ┃ Size ┃ Dedup Ratio ┃ Ratio ┃ Bytes ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━┩
│ Parquet │ 16.2 GiB │ 15.0 GiB │ 13.4 GiB │ 93% │ 83% │ 13.5 GiB │
│ CDC ZSTD │ 8.8 GiB │ 5.6 GiB │ 5.6 GiB │ 64% │ 64% │ 6.0 GiB │
│ CDC Snappy │ 16.2 GiB │ 8.6 GiB │ 8.1 GiB │ 53% │ 50% │ 9.4 GiB │
└────────────┴────────────┴────────────┴──────────────────────┴─────────────┴─────────────────────┴──────────────────────┘

Some results calculated on all revisions of food.parquet

❯ de stats /tmp/food --max-processes 4 Writing CDC Parquet files with ZSTD compression 100%|█████████████████████████████████████████████████████████████████████████████████████| 32/32 [10:28<00:00, 19.64s/it]
Writing CDC Parquet files with Snappy compression 100%|█████████████████████████████████████████████████████████████████████████████████████| 32/32 [08:11<00:00, 15.37s/it]
Estimating deduplication for Parquet Estimating deduplication for CDC ZSTD Estimating deduplication for CDC Snappy ┏━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┃ ┃ ┃ Compressed Chunk ┃ ┃ Compressed Dedup ┃ Transmitted XTool ┃
┃ Title ┃ Total Size ┃ Chunk Size ┃ Size ┃ Dedup Ratio ┃ Ratio ┃ Bytes ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━┩
│ Parquet │ 182.6 GiB │ 148.0 GiB │ 140.5 GiB │ 81% │ 77% │ 146.4 GiB │
│ CDC ZSTD │ 107.1 GiB │ 58.0 GiB │ 57.9 GiB │ 54% │ 54% │ 66.2 GiB │
│ CDC Snappy │ 176.7 GiB │ 79.6 GiB │ 77.2 GiB │ 45% │ 44% │ 101.0 GiB │
└────────────┴────────────┴────────────┴──────────────────────┴─────────────┴─────────────────────┴──────────────────────┘

Chunk size shows the actual storage required to store the CDC chunked parquet files in a simple CAS implementation.

What changes are included in this PR?

A new column chunker implementation based on CDC algorithm, see more details in the docstrings. The implementation is added to the C++ Parquet writer and exposed in PyArrow as well.

Are these changes tested?

Yes. Tests have been added to the C++ implementation as well as the exposed PyArrow API.

Are there any user-facing changes?

There are two new parquet writer properties on the C++ side:

  • enable_content_defined_chunking() to enable the feature
  • content_defined_chunking_options(min_chunk_size, max_chunk_size, norm_factor) to provide additional options

There is a new pq.write_table(..., use_content_defined_chunking=) keyword argument to expose the feature on the Python side.

I marked all user-facing changes as EXPERIMENTAL.

Comment threadcpp/src/parquet/column_chunker.h Outdated
@github-actionsgithub-actionsBot added awaiting changes Awaiting changes and removed awaiting committer review Awaiting committer review labels Jan 27, 2025
Comment threadcpp/src/parquet/column_chunker.h Outdated
@github-actionsgithub-actionsBot added awaiting change review Awaiting change review and removed awaiting changes Awaiting changes labels Jan 27, 2025

@mapleFUmapleFU left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is cdc a part of the parquet spec? Or is it a poc?

@kszucs

kszucs commented Jan 28, 2025

Copy link
Copy Markdown
MemberAuthor

Is cdc a part of the parquet spec? Or is it a poc?

It is not. You can think of it as an implementation specific feature similar to the existing options to specify how record batches and pages are being split.

@kszucs
kszucsforce-pushed the content-defined-chunking branch 2 times, most recently from 9919b4f to 6fc058dCompareFebruary 6, 2025 15:49
Comment threadcpp/src/parquet/column_chunker.h Outdated
@github-actionsgithub-actionsBot added awaiting changes Awaiting changes and removed awaiting change review Awaiting change review labels Feb 7, 2025
Comment threadcpp/src/parquet/column_chunker.h Outdated
@github-actionsgithub-actionsBot added awaiting change review Awaiting change review and removed awaiting changes Awaiting changes labels Feb 7, 2025
@rok

rok commented Feb 11, 2025

Copy link
Copy Markdown
Member

Thanks for doing this @kszucs ! I like how this doesn't need any changes to readers.

Questions:

  • As it stands in this PR, CDC is either on or off for all columns. How about enabling it per column? In general case some columns might not be worthy candidates for it.
  • Use case described in HF blogpost describes cases where rows are added or removed but not much else is changed. Wouldn't it then make sense to first try a shortcut deduplication where if we identify a duplication in the first column we first check for the same duplication at the same indices in all other columns before running a full hashing pass?

@kszucs
kszucsforce-pushed the content-defined-chunking branch 2 times, most recently from e82efee to f854ee8CompareFebruary 20, 2025 14:01
@github-actionsgithub-actionsBot added awaiting changes Awaiting changes and removed awaiting change review Awaiting change review labels Feb 20, 2025
@kszucs
kszucsforce-pushed the content-defined-chunking branch from f854ee8 to 58e1a82CompareFebruary 21, 2025 16:09
@github-actionsgithub-actionsBot added awaiting change review Awaiting change review and removed awaiting changes Awaiting changes labels Feb 21, 2025
@kszucs
kszucsforce-pushed the content-defined-chunking branch from 58e1a82 to 7556467CompareFebruary 21, 2025 16:10
@kszucs
kszucs marked this pull request as ready for review February 24, 2025 20:47
@kszucs

Copy link
Copy Markdown
MemberAuthor

@github-actions crossbow submit test-conda-cpp-valgrind

@github-actions

Copy link
Copy Markdown

Revision: 1cc2e4b

Submitted crossbow builds: ursacomputing/crossbow @ actions-6a43d39b56

TaskStatus
test-conda-cpp-valgrindGitHub Actions

@kszucs

kszucs commented May 13, 2025

Copy link
Copy Markdown
MemberAuthor

I collected the possible follow-ups, once the PR is merged I will create the corresponding tickets:

@pitrou

Copy link
Copy Markdown
Member

@github-actions crossbow submit preview-docs

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Congratulations @kszucs :)

@github-actions

Copy link
Copy Markdown

Revision: 1cc2e4b

Submitted crossbow builds: ursacomputing/crossbow @ actions-81dc2a98bf

TaskStatus
preview-docsGitHub Actions

@wgtmacwgtmac left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

+1 (for the C++ part)

@kszucs

Copy link
Copy Markdown
MemberAuthor

Thanks @pitrou@wgtmac@kou@mapleFU for the reviews!

@conbench-apache-arrow

Copy link
Copy Markdown

After merging your PR, Conbench analyzed the 4 benchmarking runs that have been run so far on merge-commit dd94c90.

There were no benchmark performance regressions. 🎉

The full Conbench report has more details. It also includes information about 8 possible false positives for unstable benchmarks that are known to sometimes produce them.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@kszucs@rok@kou@mapleFU@wgtmac@pitrou