perf(precomputed): re-enable multithreading for local file reads/writes - #700
perf(precomputed): re-enable multithreading for local file reads/writes#700nkemnitz wants to merge 3 commits into
Conversation
|
I think this would need careful benchmarking. The advantage of turning off threads was very large. |
ef0f6a4 to
bd45899
Compare
🤷♂️Looks like there is 20ms overhead for creating the ThreadedQueue. Claude found a way to reeduce it a little bit by disabling the progress bar. I added a gate based on chunk count. The gzip speedup with this PR alone won't be visible, yet. https://github.com/dcwatson/deflate already pushed the changes to main, but hasn't pushed a new release, yet. For other libs, such as libjpeg-turbo, this change already helps. 🤖 Benchmark resultsGCP Ratio = threads OFF (master) / threads ON (this PR); >1 means the PR helps.
Confirms the premise: nothing without the GIL release, ~4-5x with it. Single-thread throughput is identical between the two builds, so this is purely scaling. Separately, jpeg layers already get ~7x in both builds — libjpeg-turbo releases the GIL today, so that part of the win is available regardless of what The case that needed guardingThreading isn't free: there's a fixed ~20 ms cost per batch, which is more than a small cutout spends decompressing. Read latency vs chunk count:
The 1-chunk case is safe only because About half that fixed cost is pure waste:
Break-even moves from ~16 chunks to ~8, and large reads are unaffected. Small writes never regress (1.00x at 1 chunk, 1.03x at 2, 1.91x at 4, 3.32x at 8) — compression is ~5.5x more expensive per byte than decompression, so the batch cost is amortized immediately. The write path needs no threshold. Pushed in responseRebased onto
CaveatThe gzip win needs an unreleased |
Standing up the thread pool costs a fixed ~20ms per batch, which is more than a small cutout spends decompressing. Below MIN_THREADED_DISK_READS chunks a threaded local read is slower than a serial one, so gate on the number of chunks rather than the protocol alone. Also thread cache-hit reads, which were pinned to concurrency=0 and so missed the same speedup on cached remote layers, and keep greenlets on the serial path since they cannot overlap C-level (de)compression. ThreadedQueue.wait() polled at 15ms intervals solely to advance a tqdm bar that is disabled on this path; skipping the poll when there is no bar to draw removes about half the per-batch cost. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
bd45899 to
4778421
Compare
|
One note: skipping the sleep cycle means that errors are not raised until all elements are processed, meaning it can be a long time to see an error. |
|
This was some very early threading work I did. Probably it should be using a condition variable or something to signal errors/task completion. |
|
What do you think of this approach for the sleepiness? |
wait() spun at 15ms intervals to advance a tqdm bar that is disabled when progress is off, adding fixed latency to every batch. Block on a condition variable instead, notified when the last outstanding task completes or when a worker posts an error. Errors still surface immediately rather than only after the queue drains, and an idle batch costs no wakeups at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Commented on #703 and added a commit that addresses the delayed error issue |
|
deflate 0.9.0 is now on PyPI 🎉 |
|
I wonder if we should gate this functionality behind whether the python version is free threaded or not |
With seung-lab/cloud-files#126 compression/decompression running GIL-free, preserving the ThreadPool also makes sense for local precomputed layers. (And it would for mem and LRU cache if we ever decide to allow compressed data there, too)