Skip to content

[FIX] Overlap chunk uploads across documents and carry the thread count into workers - #24

Closed
noel-improv wants to merge 3 commits into
mainfrom
fix/s3-doc-store-upload-concurrency
Closed

[FIX] Overlap chunk uploads across documents and carry the thread count into workers#24
noel-improv wants to merge 3 commits into
mainfrom
fix/s3-doc-store-upload-concurrency

Conversation

@noel-improv

Copy link
Copy Markdown
Owner

Description

S3ChunkUploader waited for one source document's chunk uploads before submitting the next document's, so in-flight uploads were bounded by a single document's chunk count rather than by the thread pool. On the WikiHow benchmark corpus that is 3.31 chunks per document, so most of the pool sat idle whatever it was sized to. A thread sweep measured the write phase at ~13 minutes for 16, 32 and 64 threads alike, while the extraction phase either side of it halved with each doubling.

The pool was also getting the wrong size. S3BasedDocs read GraphRAGConfig.extraction_num_threads_per_worker at upload time, and that read happens in a worker spawned by run_pipeline, which inherits no parent memory. A run asking for 64 uploaded with the default of 4.

End to end this measures 1.15x on the same LLM work, with byte-identical output. A replay against a stand-in S3 client shows a far larger gain, but it sizes the pool in-process and so assumes away the pool question, which this PR does not address.

Changes

  • S3ChunkUploader.upload keeps a bounded window of documents in flight instead of draining one document before submitting the next. Documents are still yielded in order, and still only once their own chunk uploads have been attempted.
  • Thread count is resolved in S3BasedDocs.__init__, which runs in the process that configured GraphRAGConfig, and carried as a field so it pickles with the handler. This is the trip an extractor's num_workers already survives.
  • A ConfiguredThreadCount mixin holds the single definition of the count-or-config fallback. Both uploaders and both downloaders use it, so read back is sized by whatever configured the writer rather than by a config read in the wrong process.

Problem

Uploads to the S3 doc store did not scale with extraction_num_threads_per_worker, for two independent reasons: the uploader never had enough work queued to use the pool, and the pool was sized from a config read that returned the default in the worker process.

Related issue (if any): #

Testing

  • Unit tests added/updated
  • Integration tests added (as appropriate)
  • Existing tests pass (pytest)
  • Tested manually (describe below)

pytest tests/unit on this branch: 1981 passed. The one error, test_integ_dependency_compatibility::test_boto3_botocore_version_alignment, is a pre-existing pip-resolution failure unrelated to this change.

New tests cover overlap across documents (a threading.Barrier that only releases on genuinely simultaneous uploads, so it fails against the serial version), yield order, every chunk uploaded exactly once, the INDEX_KEY skip, a failed chunk not stopping the stream, and the thread count surviving into an uploader and a downloader whose process sees the default.

Measured against the WikiHow benchmark corpus on real S3, output byte-identical to the previous implementation.

Checklist

  • Code follows existing style and conventions
  • License headers present on new files
  • Documentation updated (if applicable)
  • No breaking changes (or clearly documented)

One behaviour change worth naming: num_threads is now resolved when S3BasedDocs is constructed, so setting GraphRAGConfig.extraction_num_threads_per_worker afterwards no longer affects it. The call sites in benchmarks/scripts/benchmark_extract.py and the integration-test scripts take the value from the environment, which the config reads lazily on first access, so none of them are affected.

A failed chunk upload is logged and not raised, and its document is still yielded. That is unchanged behaviour, but it means a yielded document does not guarantee every one of its chunks reached S3. The docstring now says so rather than claiming durability.


By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

S3ChunkUploader waited for one source document's chunk uploads before
submitting the next document's. In-flight uploads were therefore bounded by
a single document's chunk count rather than by the thread pool - 3.31 chunks
per document on the WikiHow benchmark corpus - so most of the pool sat idle
whatever it was sized to.

A thread sweep on that corpus measured the write phase at ~13 minutes for
16, 32 and 64 threads alike, while the extraction phase either side of it
halved with each doubling.

Keep a bounded window of documents in flight instead. End to end on the same
LLM work that measures 1.15x, with byte-identical output. Replaying the corpus
against a stand-in S3 client shows a far larger gain, but it sizes the pool
in-process and so assumes away the pool question this change does not address.

Documents are still yielded in order, and still only once their own chunk
uploads have been attempted. A failed chunk is logged rather than raised,
as before.
S3BasedDocs sized its upload pool from GraphRAGConfig at upload time. That
read happens in a worker spawned by run_pipeline, which inherits no parent
memory, so a thread count set programmatically on the config was absent and
read back as the default of 4. A benchmark asking for 64 uploaded with 4.

Resolve the count in __init__, which runs in the process that configured
GraphRAGConfig, and carry it as a field so it pickles with the handler. This
is how an extractor's num_workers already survives the same trip.

Callers can pass num_threads explicitly. Uploaders built directly still fall
back to reading the config, so nothing outside this path changes.

The chunk downloader reads the config the same way and is left alone; the
write path is the one with measurements behind it.
@github-actions

Copy link
Copy Markdown

Lexical Graph Coverage Report: The coverage is at 65.02% (target: 80%). Download the HTML report here.

@github-actions

Copy link
Copy Markdown

Lexical Graph Coverage Report: The coverage is at 65.02% (target: 80%). Download the HTML report here.

… readback

The uploaders each had their own copy of the num_threads fallback, and the
two downloaders still read GraphRAGConfig at use time - the read this branch
calls broken in a spawned worker. A ConfiguredThreadCount mixin holds the one
definition and all four components take it, so read back is sized by whatever
configured the writer.

Also drops the benchmark figures from S3ChunkUploader.upload's docstring and
corrects what it promises: _drain logs a failed chunk rather than raising, so
a yielded document is one whose uploads were attempted, not one whose chunks
are all in S3.
@noel-improv
noel-improv force-pushed the fix/s3-doc-store-upload-concurrency branch from 77ee254 to a8883b8 Compare August 25, 2026 16:11
@github-actions

Copy link
Copy Markdown

Lexical Graph Coverage Report: The coverage is at 65.02% (target: 80%). Download the HTML report here.

@noel-improv

Copy link
Copy Markdown
Owner Author

Superseded by awslabs#507, which tracks the same branch. Closing this one so review happens in a single place.

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.

1 participant