Add opt-in small-blob read coalescing to GrpcStore#2540
Open
erneestoc wants to merge 1 commit into
Open
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
When `experimental_read_batching` is configured on a gRPC store, full reads of small blobs are coalesced into BatchReadBlobs RPCs instead of issuing one ByteStream Read stream per blob. Each ByteStream read carries a ~1.6ms fixed per-RPC cost; batched reads measured 52us/blob at 4KiB (30.2x), 4.8x at 32KiB and 1.8x at 256KiB against a real in-process gRPC server. The coalescer uses slot-based group commit with no timers: callers enqueue their read and try_acquire one of `dispatch_slots` semaphore permits to become a dispatcher. A dispatcher drains up to `max_batch_bytes` of pending reads (grouped by digest function, with a per-entry overhead charge to bound entry counts) into a single BatchReadBlobs request, deduplicates digests and fans response data out to every waiter of a digest, validates entry size and identity compression, and keeps draining until the queue is empty. After releasing its permit the dispatcher re-checks the queue to close the race with concurrent enqueues. When more than `max_queued_bytes` are already waiting, new reads bypass batching and use the stream path so the coalescer never blocks. Per-item failures (e.g. NOT_FOUND) fail only that read; retryable per-item errors fall back to the ByteStream path, which re-enters the retry machinery; whole-RPC failures are broadcast to the batch. Dispatchers run as detached background tasks holding a strong reference obtained via a weak self pointer: cancellation of any individual reader (timeouts, try_join siblings) can neither abort an in-flight batch RPC nor strand still-queued waiters - a cancelled caller only drops its own result receiver. Because batched reads share one upstream RPC across many client requests, `experimental_read_batching` is rejected at construction when combined with `forward_headers`, whose per-client values (e.g. credentials) cannot be attached to a shared RPC. The coalescer exports metrics under the store's read_batcher group: batches_sent, blobs_batched, queue_bypasses, batched_read_errors and a queued_bytes gauge (atomic mirror of the queue byte budget). Partial reads, blobs above `max_blob_size_bytes`, non-digest keys, AC stores and empty blobs keep the existing behavior. The feature is off by default and unset config is zero behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1f3cb75 to
5a5f305
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
GrpcStoreopens one ByteStreamReadstream per blob regardless of size. Measured against a real in-process gRPC server, each stream carries ~1.6 ms of fixed cost (stream setup, resource-name parse, server dispatch) — for the small blobs that dominate action input trees (headers, sources, small protos), that fixed cost is 30x the transfer itself. Worker cold-start materialization of a ~4400-file tree issues thousands of these.This adds opt-in coalescing of small-blob reads into
BatchReadBlobsRPCs, configured perGrpcStore:Default off: with the field unset, behavior is byte-for-byte unchanged (verified by a test asserting
BatchReadBlobsis never called).Mechanism: slot-based group commit (no timers)
Eligible
get_partcalls (digest key, full read from offset 0, size <= threshold) enqueue and try to acquire one ofdispatch_slotspermits. A permit holder drains the queue into<= max_batch_bytesrequests and demuxes responses to per-caller oneshots; ops arriving while all slots are busy accumulate and depart together when a slot frees.digest_functionis captured at enqueue time from the caller's context, never in the dispatcher.Measurements
End-to-end through this feature (
get_partpath), realByteStreamServer/CasServerover TCP, 64-wide caller concurrency:Raw RPC-shape ceiling measured separately (batch vs stream, no coalescer): 30.2x at 4 KiB — the remaining gap is queue/demux overhead. On a production iOS RBE deployment, worker->CAS reads at ~ms-scale fixed cost per blob dominate cold input materialization; this targets exactly that.
Caveat on absolute local timings: these were captured under a test harness
whose capturing TRACE subscriber adds measurable overhead, so treat them as
relative (same-harness A/B). The per-stream fixed cost and the RPC-shape
ceiling are harness-independent; production validation is below.
Scope and companion changes
This PR addresses the wire-side per-blob fixed cost (one stream per
blob), which matters most on real-RTT networks. The disk-side per-blob
fixed cost on macOS workers (a full device-cache flush per uploaded blob,
F_FULLFSYNC) is addressed independently by the FilesystemStore flushcoalescing /
sync_policychanges; the three are orthogonal and compose.Production 4-build sweep with this flag + subtree caching on macOS workers:
per-exec setup time -67% avg / -72% P95 cold (true-cold, all caches wiped),
-48% avg incremental, queue time -89%, zero errors, warm path byte-identical.
Testing
grpc_read_batching_test.rs: fake CAS + ByteStream servers over real TCP recording which RPCs are used. Covers: contents correct for 200 coalesced reads (and ByteStream not used for them); above-threshold blobs use streams; partial reads bypass; per-item NOT_FOUND isolation within a shared batch (deterministic same-batch grouping viadispatch_slots: 1); config-unset never callsBatchReadBlobs.dispatch_slots == 0rejected at construction (would strand queued reads).bazel test //nativelink-store:integration //nativelink-config:unit_testgreen (clippy + rustfmt aspects);cargo check --testsandcargo fmt --checkclean.macOS flush-coalescing (Coalesce FilesystemStore durability flushes on macOS #2539) PRs: zero conflicts, all 40 bazel tests
across config/store/worker green on the combined branch.
This change is