Skip to content

Add explicit asynchronous submit/poll offload API - #34

Merged
byrnedj merged 5 commits into
intel:mainfrom
byrnedj:async-api-upstream
Oct 27, 2026
Merged

byrnedj merged 5 commits into
intel:mainfrom
byrnedj:async-api-upstream

Conversation

@byrnedj

@byrnedj byrnedj commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

This adds a public dto.h with an explicit submit/poll API, so applications can overlap DSA work with their own CPU work. It unifies the API work from the cachelib-navy and ch-async-ops development branches into one interface, and adds libdto_explicit: the same library without the libc interposers.

Single operations (dto_async_op, caller-allocated, can be polled from any thread):

  • Data movement: dto_submit_memcpy, dto_submit_memset, dto_submit_memfill (8-byte pattern).
  • CRC32C:
    • dto_submit_crc and dto_submit_memcpy_crc return the raw CRC32C: the SSE4.2 crc32 loop from 0, no final inversion.
    • dto_submit_crc32c and dto_submit_memcpy_crc32c take a seed in the iSCSI presentation: seed 0 gives standard CRC32C, and passing a previous result chains.
    • dto_crc32c_with_seed is a synchronous call that always returns the right value, using the CPU below DTO_CRC_MIN_BYTES.
  • Compare: dto_submit_compare, dto_submit_compval (against an 8-byte pattern).
  • Translation fetch: dto_submit_transl_fetch (DSA 2.0).
  • Completion: dto_async_poll; dto_async_wait, which waits with DTO_WAIT_METHOD and is a no-op for ops that were never submitted; and the accessors (crc_val, crc32c_val, result, bytes_completed, status).

Batches (dto_batch, up to 1024 operations in one DSA batch descriptor; a single operation goes out as a plain descriptor):

  • dto_batch_create, then dto_batch_add_{memmove,memfill,dualcast,crc,memcpy_crc,compare,compval}, then dto_batch_submit, then dto_batch_wait / dto_batch_poll, with per-operation status, result, bytes completed and CRC.
  • dto_batch_poll redoes on the CPU every operation the device did not finish, results included, so DONE means every result is valid; dto_batch_status still reports what the device did.
  • A move the device stopped partway is resumed from where it stopped, in the direction it was copying. With overlapping buffers, the device has already overwritten part of the source.
  • dto_submit_batch_copy is kept as a convenience on top of the builder.

Every submit takes flags:

  • DTO_SUBMIT_CC steers output toward the CPU cache.

  • DTO_SUBMIT_BOF asks the device to block on page faults, and is passed to it as given:

    WQ block-on-fault DTO_SUBMIT_BOF Page fault
    enabled set the device waits; the op completes
    disabled set always DTO_ASYNC_FAILED, status 0x11 (invalid flags)
    either not set DTO_ASYNC_FAILED, status 0x03: partial completion (dto_async_bytes_completed)

    dto.h documents how to finish an overlapping move from where the device stopped. Nothing in the library requests block-on-fault on its own (dto_crc32c_with_seed doesn't), so the default calls work on either configuration.

  • DTO_ASYNC_FALLBACK means nothing was done, and explicit submits are not gated by DTO_MIN_BYTES.

Migrating from the development branches:

  • Cloud Hypervisor (ch-async-ops):
    • Add a trailing flags argument to dto_submit_{crc,compare,compval,transl_fetch} and dto_batch_add_{compare,crc}.
    • cache_control becomes flags; 1 is still DTO_SUBMIT_CC.
    • Pass DTO_SUBMIT_BOF, which used to be implicit. It needs a work queue with block-on-fault, as the branch's accelConfig.sh configures.
    • dto_async_crc_val now returns uint32_t.
    • dto_batch_poll no longer returns FAILED.
    • CRC values are unchanged (raw).
  • cachelib-navy:
    • dto_batch_op_new/free becomes dto_batch_create/destroy.
    • dto_submit_batch(members) becomes dto_batch_add_* calls.
    • dto_batch_member_done / dto_batch_crc_val become dto_batch_status / dto_batch_crc.
    • cache_control becomes flags.
    • The aggregator wait method and dto_thread_active (admission control) are left for follow-up PRs, so dto_wait_blocks() returns 0 for now.
    • The older blocking/callback calls (dto_memcpy_async, dto_crc, dto_memset_pages, dto_batch_copy, ...) are not part of this API.

Testing (GNR, Xeon 6787P, one DSA, shared WQ dsa0/wq0.0):

  • Functional: the suite now covers every submit and batch operation, the CPU repair of operations that hit unmapped pages (including overlapping moves left half done in both directions), and two threads polling one batch.
    • The tests used to accept FAILED/FALLBACK everywhere, so they could not tell a working device from one rejecting every operation (as the device does with every DTO_SUBMIT_BOF op on a work queue without block-on-fault, which is what those tests passed). With DTO_TEST_EXPECT_DSA=1, offload is now required.
    • A new test_async_bof checks the table above against whichever setting the work queue has.
    • 29/29 pass in strict mode as root on a WQ with block-on-fault and on one without, for both libdto and libdto_explicit, and on the CPU-fallback path.
  • Transparent path: ctest -R sanity A/B against perf_baseline: every cell within ±1.1%, no regressions.
  • Explicit API (one thread, block-on-fault WQ, median of 3–5 runs; all buffers prefaulted, so no page faults):
    • Throughput with 32 ops in flight matches cachelib-navy and ch-async-ops for every op and size, up to 64 GB/s; for comparison, CPU memcpy reaches 14.8 GB/s and CPU CRC32C 8.5 GB/s.
    • The 64 × 64 KiB batch copy takes 68.7 µs, against 69.1–69.2 µs on the other branches.
    • ch-async-ops's own 4 KiB batch-compare benchmark gives 131 ns/page for batches of 256 or more on both branches, against 320 ns/page for CPU memcmp.
    • Latency with one op in flight is within 2% at 64 KiB and about 3% at 4 KiB (20–30 ns, for example a 4 KiB CRC at 0.92 µs against 0.89). Most of that is upstream's per-submit round-robin WQ choice in get_wq() (an atomic increment), which the development branches replace with a per-thread WQ.
  • The development branches on a WQ without block-on-fault (as this repo's accelConfig.sh configures it): every explicit op and batch fails with status 0x11 (invalid flags), because they set block-on-fault on every descriptor. Here it is set only when the caller asks for it.

@jsydir jsydir left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes sense to add an optional parameter to the dto_submit_* functions to pass in the max number of retries to allow users to selectively override the default value

dto_submit_common is not made externally visible. It should be renamed to async_submit_common (or some other name that does not start with dto_ ) to conform with the naming convention that non-transparent externally visible functions are named dto_*

libdto so far only accelerated memcpy/memmove/memset/memcmp transparently
via LD_PRELOAD. This adds a public dto.h with an explicit submit/poll API so
applications can overlap DSA data movement with their own CPU work:

  dto_submit_memcpy / dto_submit_memset / dto_submit_memcpy_crc /
  dto_submit_crc  -> dto_async_poll -> dto_async_crc_val
  dto_batch_op_new / dto_submit_batch_copy / dto_batch_poll (up to 64 copies)

The operation state is caller-allocated (opaque dto_async_op) so it can be
polled from any thread. Every submit takes a flags argument: DTO_SUBMIT_CC
directs output toward the CPU cache when the device supports it, and
DTO_SUBMIT_BOF makes the device block on page faults instead of aborting
(requires a work queue configured with block-on-fault). Without BOF a
faulting operation surfaces as DTO_ASYNC_FAILED and the caller redoes it on
the CPU; submission returns DTO_ASYNC_FALLBACK when DSA is unavailable or
the request is out of range, with nothing copied. CRC results follow the
raw CRC32C convention (seed 0, no final inversion), matching _mm_crc32.

The memset entry point takes (dest, c, n) like the C library, replacing the
earlier page-oriented helper so the offload API is uniform across operations.

Adds seven functional tests (copy, memset, crc, copy+crc, no-BOF fault
contract, argument validation, batch) that pass both on DSA hardware and on
the CPU-fallback path.
byrnedj and others added 4 commits October 21, 2026 09:34
Bring the asynchronous API work from the two development branches into
one interface, keeping this PR's conventions: every submit takes a flags
argument (DTO_SUBMIT_CC, DTO_SUBMIT_BOF), DTO_ASYNC_FALLBACK means nothing
was done, and explicit submits are not gated by DTO_MIN_BYTES.

From ch-async-ops (Cloud Hypervisor):
- dto_submit_memfill, dto_submit_compare, dto_submit_compval and
  dto_submit_transl_fetch, with dto_async_result,
  dto_async_bytes_completed and dto_async_status.
- The dto_batch builder: dto_batch_create (up to DTO_BATCH_MAX = 1024
  operations), add_memmove/memfill/dualcast/crc/memcpy_crc/compare/
  compval, submit, poll and per-operation status, result,
  bytes_completed and crc. A single operation goes out as a plain
  descriptor.  It replaces dto_batch_op; dto_submit_batch_copy remains
  as a convenience on top of it.

From cachelib-navy:
- Seeded CRC32C in the iSCSI presentation, dto_submit_memcpy_crc32c,
  dto_submit_crc32c and dto_async_crc32c_val, and the synchronous
  dto_crc32c_with_seed (gated by the new DTO_CRC_MIN_BYTES, default
  DTO_MIN_BYTES).
- dto_async_wait and dto_batch_wait, which wait with the configured
  DTO_WAIT_METHOD and are no-ops for ops that were never submitted, and
  dto_wait_blocks (0 until a blocking wait method exists).

dto_async_crc_val and dto_batch_crc now return the raw CRC32C (the
SSE4.2 crc32 loop from 0, no final inversion), which both branches and
their users' CPU fallbacks use, instead of standard CRC32C; the seeded
calls provide the standard form.

dto_batch_poll redoes every operation the device did not complete on the
CPU before returning DONE, results and CRCs included, so DONE means
every result is valid; dto_batch_status still reports the device's
status. A move the device stopped partway is resumed from where it
stopped, in the direction it was copying, because with overlapping
buffers the device has already overwritten part of the source. One
poller repairs; concurrent pollers see PENDING until it is done.

DTO_SUBMIT_BOF is passed to the device as given. dto.h now spells out
what that means for each work queue configuration: with block-on-fault
enabled the device waits for the fault; without it a BOF submit fails
with DSA_COMP_INVALID_FLAGS; and without the flag a page fault is a
partial completion, which for an overlapping move has to be finished
from where the device stopped rather than redone.

Build with DTO_NO_LIBC_INTERPOSE to leave out the memcpy, memset,
memmove and memcmp interposers (libdto_explicit).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Mv7xKtQPkGoXQUvvuuhAEz
libdto_explicit is dto.c built with DTO_NO_LIBC_INTERPOSE, for programs
that call the dto_* API directly and must not have every large libc
copy in the process sent to DSA (Cloud Hypervisor links it). CMake
builds and installs it by default (DTO_BUILD_EXPLICIT) and exports
DTO::dto_explicit; the Makefile gains a libdto_explicit target. Also
drop the duplicate include(GNUInstallDirs).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Mv7xKtQPkGoXQUvvuuhAEz
Test every submit and batch operation against CPU results: raw and
seeded CRCs (including chaining and the CRC32C check value), compare
and compval offsets, memfill patterns, translation fetch, waits on
unsubmitted ops, batch limits, the single-descriptor path, and the CPU
repair of a batch whose operations hit unmapped pages, including
overlapping moves the device left half done in both directions and two
threads polling the same batch.

test_async_bof checks the block-on-fault contract on whatever the work
queue is configured with: a BOF copy into untouched pages completes on
a block-on-fault queue and is rejected (invalid flags) on one without;
without the flag the copy stops at the first untouched page with the
touched part done; and batch operations follow the same rules.

The tests accepted DTO_ASYNC_FAILED and FALLBACK everywhere, so they
passed while every DTO_SUBMIT_BOF operation failed on a work queue
without block-on-fault. With DTO_TEST_EXPECT_DSA set they now require
operations on faulted-in buffers to be submitted and completed by the
device, and the general tests no longer request block-on-fault, so
they hold on any work queue configuration. The suite also runs against
libdto_explicit (functional_explicit).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Mv7xKtQPkGoXQUvvuuhAEz
@byrnedj
byrnedj force-pushed the async-api-upstream branch from 80d602f to de1f694 Compare October 21, 2026 16:53
@byrnedj
byrnedj merged commit 7770e18 into intel:main Oct 27, 2026
1 check passed
byrnedj added a commit to byrnedj/DTO that referenced this pull request Oct 27, 2026
Resolves conflicts with the explicit asynchronous API (intel#34), which added
to the same spots in CMakeLists.txt, Makefile, README.md and TESTING.md.
Both sides are kept: the libdto_explicit target, test and docs first,
then the dtoctl ones. The Makefile's install target becomes
"install: dtoctl install-man" with the libdto_explicit target above it.
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.

2 participants