From 75ceb98d187d63c3790f6b070d5aa54c6d3aabd9 Mon Sep 17 00:00:00 2001 From: ak2k <19240940+ak2k@users.noreply.github.com> Date: Sat, 20 Jun 2026 12:08:20 -0400 Subject: [PATCH] perf(extract): compress shards with zstd instead of snappy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch the Parquet writer from snappy to zstd (level 3). On a 184k-work sample (all 19 relationship tables, 2026-03-29 partition) total shard size drops 27% (61.6 MB -> 45.2 MB); at higher levels zstd:6 is -31% and zstd:9 -33%. Reads are no slower — pyarrow full-table read and a DuckDB group-by over work_referenced_works are both within noise of snappy (smaller files offset the marginally higher decode cost). Extraction is JSON-parse-bound, so the added compression CPU is <5% of wall-clock at level 3. The win is mostly storage and HuggingFace upload bandwidth at snapshot scale. zstd is self-describing per file, so existing snappy shards stay readable and only newly written shards use zstd — no re-extraction needed. The worker memory model comment already assumed a zstd compressor; this makes the code match it. --- sync/extract.py | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/sync/extract.py b/sync/extract.py index 42e8ff7..9ae341f 100644 --- a/sync/extract.py +++ b/sync/extract.py @@ -64,6 +64,17 @@ _PROGRESS_INTERVAL = 1_000_000 _DEFAULT_WORKERS = 6 +# Parquet compression for extracted shards. zstd at a low level is ~30% smaller +# than snappy on this data (mostly integer ID columns that Parquet already +# dictionary/RLE-encodes, plus the text-heavy abstracts table) while reading at +# least as fast — the smaller files mean less I/O on every downstream scan and, +# at snapshot scale, materially less storage and HuggingFace upload bandwidth. +# Level 3 is the balance point: levels above it buy only a few more percent for +# a growing CPU cost, and extraction is JSON-parse-bound so the codec barely +# moves wall-clock either way. +_PARQUET_COMPRESSION = "zstd" +_PARQUET_COMPRESSION_LEVEL = 3 + # Memory model: reserve ~2 GB for the OS and assume each worker peaks # around 6 GB resident (zstd compressor + PyArrow batch + Python heap). # Empirically, 8 workers on 16 GB causes swap thrashing; 8 on 64 GB is fine. @@ -899,7 +910,12 @@ def _ensure_writer(self) -> pq.ParquetWriter: write_dir = self.staging_dir if self.staging_dir else self.output_dir write_dir.mkdir(parents=True, exist_ok=True) out_path = write_dir / self._shard_name - self._writer = pq.ParquetWriter(out_path, self.schema, compression="snappy") + self._writer = pq.ParquetWriter( + out_path, + self.schema, + compression=_PARQUET_COMPRESSION, + compression_level=_PARQUET_COMPRESSION_LEVEL, + ) self._current_path = out_path return self._writer @@ -925,13 +941,23 @@ def close(self) -> None: out_path = write_dir / self._shard_name # Write empty parquet as completion marker if self.schema is not None: - with pq.ParquetWriter(out_path, self.schema, compression="snappy") as w: + with pq.ParquetWriter( + out_path, + self.schema, + compression=_PARQUET_COMPRESSION, + compression_level=_PARQUET_COMPRESSION_LEVEL, + ) as w: pass # empty file with schema else: # No data was ever written — create a minimal empty parquet empty_schema = pa.schema([pa.field("_placeholder", pa.int64())]) empty_table = pa.table({"_placeholder": pa.array([], type=pa.int64())}) - pq.write_table(empty_table, out_path, compression="snappy") + pq.write_table( + empty_table, + out_path, + compression=_PARQUET_COMPRESSION, + compression_level=_PARQUET_COMPRESSION_LEVEL, + ) self._current_path = out_path if self._writer is not None: