Skip to content

Remote I/O egress controls and retryable transport failures - #1052

Draft
karthikeyann wants to merge 2 commits into
rapidsai:mainfrom
karthikeyann:s3-remote-io-egress-and-retry
Draft

Remote I/O egress controls and retryable transport failures#1052
karthikeyann wants to merge 2 commits into
rapidsai:mainfrom
karthikeyann:s3-remote-io-egress-and-retry

Conversation

@karthikeyann

Copy link
Copy Markdown

Draft. Two commits, both driven by measurements while reading a 3 TB TPCH dataset from S3 into GPU
memory on a g7e.48xlarge (4 ENA cards, 8 GPUs) through Velox/Presto.

1. feat(remote): egress controls, streaming-store receive, and a retry-accounting fix

Unchanged from karthikeyann:s3-max-bandwidth, rebased onto current main. Adds
KVIKIO_REMOTE_IO_INTERFACE (CURLOPT_INTERFACE, if!<name> / SO_BINDTODEVICE),
KVIKIO_REMOTE_IO_DNS_SHUFFLE, KVIKIO_REMOTE_IO_NT_COPY, KVIKIO_REMOTE_IO_BUFFER_SIZE,
KVIKIO_REMOTE_IO_KTLS, KVIKIO_REMOTE_IO_DISCARD, and fixes retry accounting so a partially
completed range transfer rolls the write-callback context back before being retried.

Independent confirmation of the egress control's value, measured end to end through Presto on
SF3000 TPCH: with all four cards on one subnet the kernel picks by route metric, so every
connection left through a single NIC.

without KVIKIO_REMOTE_IO_INTERFACE with per-NIC binding
per-NIC receive [95.8, 0, 0, 0] Gbps [68.8, 45.4, 81.4, 51.0] Gbps
peak aggregate 95.8 Gbps 579.6 Gbps

One deployment note that cost some time to find: the interface name must exist in the process's
network namespace, so a containerised reader needs host networking. Without it SO_BINDTODEVICE
fails and the failure surfaces from UrlParser::parse as a misleading
url.cpp: Unsupported URL scheme on the original s3:// URL, because
encode_special_chars_in_path() parses without CURLU_NON_SUPPORT_SCHEME and outside any
try/catch. Worth improving separately.

2. fix(remote): retry transport-level curl failures (resets, DNS, partial body)

HttpRetryPolicy::is_retryable() currently accepts only CURLE_OPERATION_TIMEDOUT, with a TODO
asking for more candidates. Every other transport failure is fatal, so a single dropped packet fails
an entire multi-TB table scan. Two failure modes hit repeatedly at these rates:

  • CURLE_COULDNT_RESOLVE_HOST — the remote path resolves per request with no shared DNS cache, so at
    512 concurrent requests x 8 processes the query rate exceeds the AWS VPC resolver's 1024 packets/s
    per-ENI allowance.
  • CURLE_RECV_ERROR — S3 recycles connections; observed 6-7 times per 22-query run.

Both were fatal, and in our stack the throwing worker then crashed, so one lost packet cascaded into
unrelated query failures. Measured impact: TPCH runs went from 17/22 and 14/22 queries passing (a
different set failing each time) to 22/22 in a single run.

This now also retries CURLE_COULDNT_CONNECT, CURLE_SEND_ERROR, CURLE_PARTIAL_FILE and
CURLE_GOT_NOTHING, and stops reporting Got HTTP code 0 for failures that never carried an HTTP
status. It is only safe on top of commit 1's retry-accounting fix: perform(on_retry) rolls the
callback context back, so a retried partial transfer cannot corrupt the destination buffer.

Not included

KVIKIO_REMOTE_IO_KTLS is retained from commit 1 but confirmed still inert: setting it left
/proc/net/tls_stat TlsRxSw unchanged, consistent with libcurl installing its own BIO while
OpenSSL requires the BIO under the SSL object to advertise kTLS on a real socket fd.

Open questions for reviewers

  • Should the retryable-transport-error set be configurable rather than hard-coded?
  • Should encode_special_chars_in_path() pass CURLU_NON_SUPPORT_SCHEME so endpoint-probe failures
    stop masquerading as scheme errors?

…ccounting fix

Reading S3 over HTTPS at multi-hundred-Gbps exposed several gaps in the remote
path. Measured on a g7e.48xlarge, four ENA cards, against s3://rapids-tpch.

Retry accounting (a correctness bug). CurlHandle::perform() retries
curl_easy_perform() without rolling back what the write callback already
accepted, so a transfer that times out part way through resumes with the
destination offset still advanced, overruns the caller's buffer and returns
CURLE_WRITE_ERROR, which is not retryable. A recoverable timeout therefore
became a hard failure, reported as the misleading "maybe the server doesn't
support file ranges?". With 512 MiB ranges under four-card load this killed two
of four processes on every attempt. CurlHandle now takes an on-retry hook and
RemoteHandle::read uses it to reset the callback context.

Streaming-store receive, KVIKIO_REMOTE_IO_NT_COPY. libcurl delivers one buffer
at a time, 16 KiB by default, so every memcpy in the write callback is far below
glibc's non-temporal threshold and uses ordinary stores. An ordinary store to a
line not in cache fetches it first, so with a destination far larger than
last-level cache each delivered byte costs two DRAM accesses and the receive
path becomes memory-bandwidth-bound rather than CPU-bound. Streaming stores skip
the fetch: 669 -> 874 Gbps delivered into pinned host memory, +29%, reproduced.
Off by default because it is the wrong choice for a small destination that stays
in cache, which is exactly the device-memory bounce buffer.

CURLOPT_INTERFACE, KVIKIO_REMOTE_IO_INTERFACE. The if!<name> form binds with
SO_BINDTODEVICE, the only way to choose an egress NIC from inside the process
when a host has several cards on one subnet. Without it the kernel picks by route
metric and every connection leaves through one card.

CURLOPT_DNS_SHUFFLE_ADDRESSES, KVIKIO_REMOTE_IO_DNS_SHUFFLE. S3 publishes many
front-end addresses but libcurl connects to the first that answers, so a process
otherwise piles every connection onto one endpoint.

CURLOPT_BUFFERSIZE, KVIKIO_REMOTE_IO_BUFFER_SIZE, default unchanged. Raising it
looks right and measures worse, because libcurl allocates this buffer per easy
handle and there is one handle per in-flight sub-range: at 4096-way concurrency
that is 64 MiB at 16 KiB against 4 GiB at 1 MiB, and the smaller total stays in
L3. Measured 862 Gbps at 16 KiB, 745 at 256 KiB, 727 at 1 MiB. Exposed because
the trade-off inverts at low concurrency.

KVIKIO_REMOTE_IO_KTLS sets SSL_OP_ENABLE_KTLS. It does not currently engage
through libcurl, which installs its own BIO while OpenSSL requires the BIO under
the SSL object to advertise kTLS on a real socket fd; TlsRxSw stays at zero.
Kept off by default and left in place for when libcurl gains support.

KVIKIO_REMOTE_IO_DISCARD accounts for bytes without copying them, to separate
transport cost from destination cost. Benchmarking only: the accounting stays
honest but the destination holds garbage. It measured the destination write at
31% of achievable throughput, which is what motivated the streaming-store path.

All options are read once into function-local statics, because a handle is
constructed per sub-range transfer and getenv would otherwise sit on the hot
path.
@copy-pr-bot

copy-pr-bot Bot commented Aug 30, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

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