Expose manifest update concurrency configuration option#2274
Conversation
|
This looks good @aldenks ! Can you reformat the code so it passes our Code Quality check? Thank you! |
|
Thanks! @paraseba thinking a step ahead, this PR makes the i/o concurrent, but it does not touch the concurrency of the manifest decompress, update, recompress portion which is serial on a single core at the moment. That's the current bottleneck to virtual commits after this merges and will become more of one as we move to ~400 array datasets. I mention it now because exposing cpu parallelism for compression would mean more config options, or would interact with this config option naming here. A change now might make that future update easier. Some possibilities
Thoughts? |
I don't think that's entirely true? I don't have time right now to review the code, but iirc this number will make the full processing of manifests to be concurrent, including the CPU parts. For example, by having multiple executions of Am I wrong? Of course, it will still be a limiting factor because there is actual work being done in these methods, so you won't get linear speed up. |
|
I might be misremembering my profiling. I'll look into it. |
|
@paraseba the decompression of manifests is parallelized (#2164), the manifest merge/rebuild/recompress is currently CPU-bound to one core. tl;dr, if supporting 200+ array stores w/ fast commits will at some point become a priority for icechunk, some cpu concurrency on the manifest merge+recompress path will probably become needed. I'm not advocating for implementing that now, just want to have it in mind when naming this config option. Reasonable options in my mind
What do you think? Some profiling results I had claude do after prototyping a more cpu concurrent implementation to make sure this was real: Setup: 64 arrays × 150k refs each (9.6M refs); commit touches 1 ref in every array → all 64 manifests rewritten. In-memory store behind
Per-step CPU breakdown for one node's manifest update (150k refs, zstd level 3):
Appendix: where current serial comes from
The contrast — decode is not serial — fetch_and_decode does zstd::decode_all + deserialize on spawn_blocking, gated at available_parallelism (#2164): |
There was a problem hiding this comment.
Thank you so much for the careful attention to detail @aldenks , and I'm sorry it took me this long to get back to this.
I think I have a made a decision about:
- what to name this
- in which config struct to put it
- a small change on how to use the config
See my inline comments for details.
Let me know what you think.
Thank you.
| let max_concurrent_nodes = self | ||
| .max_concurrent_nodes | ||
| .unwrap_or_else(|| { | ||
| self.session.config().max_concurrent_manifest_updates() as usize |
There was a problem hiding this comment.
I'm not a fan of having this low level datastructure going all the way back to the higher level config. Let's go back to creating this datastructure with a non optional parameter, and use the config at creation/edit time.
|
|
||
| /// How many array nodes have their manifests updated concurrently during a | ||
| /// commit, amend, flush, or rewrite_manifests. | ||
| #[serde(default)] |
There was a problem hiding this comment.
I think there is a better place for this config attribute. Let's add it as:
ManifestConfig::max_concurrent_manifest_fetches_during_commitIcechunk updates manifests serially during a commit: do_flush drives one future per array node with buffer_unordered(max_concurrent_nodes), but CommitBuilder hardcoded that to 1 and the Python bindings never overrode it, so every Python commit/amend/flush ran fully serial. On datasets with many arrays this makes commits far slower than necessary. Add ManifestConfig::max_concurrent_manifest_fetches_during_commit (Option<u16>, default 1), used as the default for every manifest-update path: commit, amend, flush, and rewrite_manifests. CommitBuilder resolves it from the session config when the builder is created; the existing .max_concurrent_nodes(n) builder method stays as a per-call override. Values are clamped to at least 1, since buffer_unordered(0) yields an empty stream that would silently drop all manifests. The field is mirrored in the Python ManifestConfig binding (+ .pyi + docs). rewrite_manifests now takes Option<usize> and the Python binding passes None so it follows the config default instead of a hardcoded 1. The JS binding passes manifest config through serde, so it picks the field up unchanged. Closes earth-mover#2273 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
6d7eee0 to
9757176
Compare
Closes #2273
Allow users to tune the concurrency used when updating manifests.
A couple design decisions for feedback:
RepositoryConfignext to existingmax_concurrent_requestsandget_partial_values_concurrency.max_concurrent_manifest_updates-- no strong opinions here1, matching current behavior.min(16, available_parallelism()). Anything higher than roughly 16 doesn't help,do_flushruns all nodes on a single tokio task, so their I/O waits are overlapped but the CPU work (decode/merge/rebuild/compress) still serializes on one core.