Local file transfers#
File segments are available in every build. Linux offers three paths for registered memory to read or write a local file.
Path |
Cargo features |
Qualification |
|---|---|---|
DRAM ↔ file |
|
Qualified on real ext4 and tmpfs files in CI |
CUDA ↔ pinned DRAM ↔ file |
|
Implemented. Installed GPU/NVMe gate unrun |
CUDA or DRAM ↔ file through cuFile |
|
Implemented. Installed native GDS gate unrun |
With both executors enabled, Transport::Unspec prefers eligible GDS, then
io_uring. With neither feature, opening and inspecting a segment still works.
File READ and WRITE return Unavailable before payload I/O.
Qualification and gate commands
CI runs cargo tree --locked --features "$FEATURES" -e normal,build for
builds without io-uring and checks that the io-uring crate is absent.
The GNU/Linux runtime control checks shared-library lookups, threads and live
ring fds after engine construction and file inspection. An idle engine has no
storage worker or ring. File submission probes the kernel when a policy first
considers io_uring. File inspection and memory registration do not load cuFile.
File segments#
TransferEngine::open_file_segment reserves an engine-local name. The
literal text after file:// is a filesystem path. An absolute path starts
with a third slash, as in file:///tmp/cache.bin. A relative path resolves
in the process working directory. The first descriptor lookup opens the
existing regular file. Opening a segment never creates or truncates a file.
This example checks the four-byte range starting at byte 8 of an existing file and closes the segment.
let segment = engine.open_file_segment("file:///tmp/cache.bin", Permission::RemoteRead)?;
let info = engine.file_segment_info(&segment)?;
assert_eq!(info.offset, 0);
let offset = engine.resolve_file_offset(&segment, TransferOp::Read, 8, 4)?;
assert_eq!(offset, 8);
engine.close_file_segment(&segment, Duration::from_secs(1))?;
A file segment describes one buffer with base zero. The request offset counts
bytes from the start of the file. Range checks reject zero length, arithmetic
overflow and an end beyond the current file size. Choose RemoteRead,
RemoteWrite or RemoteReadWrite at open. A disallowed operation returns
Error::PermissionDenied.
Opening the same name with the same permission shares the entry.
file_segment_info caches size for up to one hour. Use refresh_file_segment
after a resize to get a fresh snapshot. Range checks always read the current
size. Once resolved, the handle stays bound to the same file across rename or
unlink. Close and reopen the name to bind a replacement file.
EngineConfig::max_file_segments bounds open and draining names (default
1024, maximum 4096). A timeout from close_file_segment leaves all aliases
closed to new operations. Retry with the same handle after admitted users
finish. A successful close frees the slot and invalidates the old handle.
Requester and whole-engine shutdown also drain file entries.
Gather and scatter across registrations#
Use TransferRequest::file_vectored when scattered local bytes map to one
contiguous file extent. List elements in file order. A
FileIoElement::Range { address, length } names one registered host or CUDA
range. FileIoElement::Padding(n) writes n zero bytes on WRITE and
discards n file bytes on READ. Padding needs no local address or
registration. Tuple-only slices of (u64, usize) remain supported.
use orbite::{FileIoElement, TransferOp, TransferRequest, Transport};
let mut elements = vec![FileIoElement::Range {
address: layer_1.base_address() + offset_1,
length: layer_bytes,
}];
if gap_bytes != 0 {
elements.push(FileIoElement::Padding(gap_bytes));
}
elements.push((layer_2.base_address() + offset_2, layer_bytes).into());
if tail_bytes != 0 {
elements.push(FileIoElement::Padding(tail_bytes));
}
let request = TransferRequest::file_vectored(Transport::Unspec, file_offset, &elements)?;
let batch = engine.submit_async(TransferOp::Write, &segment, &[request], None, timeout)?;
batch.wait(timeout)?;
let receipt = batch.task_status(0)?;
Use the same list with TransferOp::Read to restore the layers. Leading,
consecutive and trailing padding and padding-only extents are valid. Omit
zero-length elements. The limit is 1024 elements per vector and
max_batch_requests descriptors per submission. Padding counts toward those
limits, max_transfer_bytes and in-flight byte admission. Overlapping ranges
within a registration share a loan. Any CUDA range selects the CUDA
local-memory filter.
One vector takes one admission charge and produces one task receipt.
receipt.transferred_bytes includes the contiguous file prefix, even when
EOF falls inside padding. READ changes only the range bytes in that prefix.
A failed vector contributes zero bytes to the synchronous batch report.
A vectored WRITE is never replayed. An unconfirmed drain retains the file,
loans, staging memory and admission charge.
Host io_uring submits one readv or writev and uses one private scratch
lease for all padding in that vector. It zeroes the lease before each WRITE.
The lease uses the existing file staging pool or its byte-charged pageable
fallback. For CUDA, the worker zeroes or discards padding in the pinned
chunks it already holds and confirms each device copy. Both paths release
scratch under the same completion rules as data.
Aligned vectors containing only registered ranges can use cuFile directly.
WRITEs use bounded batches. Direct READ issues the next entry only after a
full receipt. Padding and unaligned vectors use io_uring when the file policy
permits it. An explicit Transport::Gds request with padding returns
Error::Unavailable, even for a page-aligned gap.
DRAM transfers with io_uring#
Enable the Linux io-uring Cargo feature. Pass &segment to
submit_and_wait, submit_async or Batch::submit. The local address is
registered DRAM and the remote address is a file offset. This reads four bytes
at offset 8.
let request = TransferRequest::new(Transport::Unspec, local.base_address(), 8, 4);
let report = engine.submit_and_wait(TransferOp::Read, &segment, &[request], timeout)?;
An unavailable kernel makes io_uring ineligible. QoS, the runtime window and the submission deadline apply to files. Local files require no receiver credit and reject attached notifications.
The caller or existing batch worker owns one ring per submission. Before
publishing any SQE it validates every request, acquires exclusive host ranges,
allocates aligned bounce buffers and checks SQ space. An overlapping host
loan fails the group before I/O. An idle asynchronous batch can submit one
contiguous host request of at most 64 KiB on its caller, when runtime queuing,
background progress and route fallback do not apply. After one nonblocking CQ
check, it may block for a CQE for at most 200 µs or the remaining request deadline,
whichever is shorter. The wait uses the ring’s timed GETEVENTS enter (or
ring-fd readiness on older kernels), with zero submissions. At most three
interrupted/unready waits share that one window; there is no spin. A CQE finishes
through the normal prefix and admission owners without creating a worker. On
expiry or an enter error, the existing batch worker takes the same ring and
drains it; it never resubmits the request. This bounded caller wait deliberately
differs from frozen TENT’s nonblocking caller status polling, so an abandoned
batch still has an independent drain owner. A failed worker start, or a worker
that exits with a submitted ring still in its ready slot, retains the ring,
fd, loans, bounce storage and admission charges and reports sticky Unquiesced.
Neither exit permits replay or marks the batch physically drained. The caller
window may expire before its first blocking reap; retained ownership depends on
the completion receipt, not on a minimum number of waits. CUDA, pinned
producer waits, vectors and larger groups keep their existing worker path.
The synchronous report counts only the
consecutive fully successful requests before the first failure, and only their
bytes. The failure span includes later SQEs submitted to the kernel. For a
WRITE, any submitted request in that span makes the remote effect Unknown.
Async tasks publish their individual CQEs. A short READ or WRITE fails with
its exact byte count, including zero at EOF. A later task may succeed. Only
the returned READ prefix is copied back.
OrbiTE reopens the retained inode with the segment’s access rights.
file_segment_info().direct_io records whether a direct fd was obtained.
TENT repairs an unaligned address with a 4 KiB bounce but passes the offset
and length through. OrbiTE uses the buffered fd for requests that violate
the filesystem’s DIO alignment. It reads STATX_DIOALIGN where available
and otherwise assumes 4096 bytes. tmpfs uses the buffered fd.
After a short submit, the worker flushes only the remaining SQEs. On a
persistent submit failure it drains the consumed prefix and fails the unsent
tail. Freeing an active batch keeps the ring and its buffers alive. If the
blocking wait fails, the owner polls the mapped CQ for the same consumed SQEs.
Cancellation stops queued work. All waits for consumed SQEs share one
cutoff, 30 seconds after the submission deadline. If a CQE is still missing
or the mapped CQ itself cannot be read, the group reports Error::Unquiesced at the first unsuccessful index
with locally_drained = false.
The ring, fds, iovecs, outstanding bounce buffers, memory loans and admission
charges stay retained. See real-file controls.
CUDA transfers through pinned host memory#
With the Linux cuda,io-uring features, use the same submission APIs with a
registered CUDA address. The caller or batch worker holds the whole device
range and consumes its producer event before access. By default the engine
leases page-aligned pinned chunks from its ProxyManager, bounded by
staging_chunk_count and staging_chunk_size. Set
EngineConfig.io_uring.pinned_staging to enable TENT’s optional file pool.
staging_slot_kb defaults to 4096 and rounds up to a 4 KiB boundary.
staging_slots defaults to 32. The checked total is at most 1 GiB.
The typed configuration or the io_uring JSON object sets these fields.
Invalid enabled sizes return a configuration error before allocating a slab.
The first eligible request creates one slab. Its slots can serve an entire
GPU request or an unaligned DRAM request. CUDA uses portable pinned memory.
The CPU platform uses ordinary page-aligned memory.
A GPU with an explicit or GPU-local NUMA placement can use the slab only if
its node matches. A request that cannot lease a suitable slot immediately uses the existing
GPU chunk pool or an aligned pageable host buffer charged against
max_inflight_bytes. Exhausting both budgets fails before submitting that
round. EngineStats.io_uring exposes slot size, capacity, active slots, hits,
misses, pinned backing and outstanding pageable bytes. A slot stays owned
until its CQE and any GPU copy are complete and the caller context is restored. The ring is sized for the device chunks and unsplit host
requests, up to max_batch_requests and the Linux submission queue limit.
Each round fills that width from the remaining pieces while chunks are
available. If none can be leased the request fails with ResourceExhausted.
WRITE confirms each device-to-host copy and restores the CUDA context before submitting the round. READ drains the SQEs first and copies back consecutive file bytes in offset order, including a short final prefix. A gap stops the byte count even if a later SQE succeeds. A submitted WRITE can still affect the file beyond that gap. The same worker releases its chunks after both I/O and CUDA completion before starting another round. An uncertain copy, event or context restore retains the affected loan, chunks, ring and admission.
Memory from cuda::allocate_pinned_host consumes its producer event and goes
directly through the host file path. Each chunk or pinned view uses the direct
fd when its address and extent satisfy DIO alignment, and the buffered fd
otherwise. The installed GPU and local NVMe profile is queued for verification.
GDS with cuFile#
Enable the Linux gds feature and pass a file segment to the existing batch
APIs. EngineConfig::gds_enabled defaults to true in a GDS build. Set it
to false to prevent all cuFile loading and registration by that engine.
When enabled, the first file submission whose policy considers eligible GDS
probes cuFile. Before discovery, stats()?.gds.mode is None.
Transport::Gds supports host and CUDA READ/WRITE. CUDA ranges register
on first GDS use in their owning context and consume producer readiness
before I/O. A clean cuFile buffer registration failure increments
EngineStats::gds.buffer_registration_failures and leaves the region available
to other transports. An explicit GDS hint for that region returns Unavailable.
Automatic selection and file policies can choose io_uring staging when allowed.
An uncertain registration still fences and retains the CUDA allocation.
Host ranges use their existing exclusive loan. A file entry owns
one cuFile handle and its O_DIRECT descriptor until close drains its users.
A failure after GDS selection is returned to the caller.
The caller or batch worker sets up one native batch at gds_batch_depth
(default 32). Requests split at 16 MiB. A group exceeding the configured
slice count fails before submission for scalar requests. Vectors span bounded batches. Each terminal cookie identifies one
slice. The group reports the completed request prefix. Each async task’s byte
count advances as contiguous receipts arrive. Its final status also accounts
for cleanup of its registration. Cancel requests native batch cancellation
and can affect other slices. Only terminal receipts authorize destruction.
An ambiguous submit drains without replay. After the deadline plus the
30-second destruction window, unresolved I/O returns Unquiesced and keeps
its registrations, loans and admission. A later submission or close polls
quarantined batches. A failed destroy or deregister retains its owners and
is never retried. Each native batch has its own handle. Group finalization always
settles metrics and retires drained admission owners (or retains the uncertain
window), including a rejected non-file group with no cuFile leases to release.
EngineStats::gds.mode reports whether the driver disables, permits or
forces compatibility I/O. FileSegmentInfo::gds_mode records that policy
when the file handle is registered. gds_require_native excludes GDS when
fallback is permitted or forced and rejects unaligned I/O or a slice exceeding
cuFile’s per-buffer cache. Physical route verification uses before/after
nvidia-fs counters in the installed gate.
The runtime loads libcufile.so.0, including its effective boolean configuration
API. Missing symbols, failed open or unreadable properties leave GDS unavailable.
Engines share one process-wide driver open. The PID guard reopens on first use
in a child and prevents reuse of parent handles. The fork control uses a shim.
NVIDIA specifies undefined behavior after fork from an initialized process.
Use a fresh executable for native child work. cuFile API reference