Skip to content

Multi-device Zero-Copy Path: Missing lowering for stream.cmd.flush/invalidate and zero-copy path #24930

Description

@Manewing

Request description

Overview

In multi-device settings that include a heterogenous device setup including e.g. the CPU and some kind of accelerator, memory is often shared between the CPU and the accelerator. While the memory may be shared, explicit flush of written memory regions may be required in case non-coherent shared memory.

Currently there exist multiple gaps in the compiler to allow a true zero-copy lowering path for this use case.

This ticket proposes steps to close these gaps and aims to describe an implementation that achieves a true zero-copy lowering path:

The zero-copy path must preserve a synchronization operation representing the handoff between devices, even when no data copy is required. Allocation planning must make the transfer result alias the source storage. Transfer canonicalization and elision must preserve required cache maintenance and producer/consumer dependencies.

The current lowering for flow.tensor.transfer

Currently handling device assignment happens through the insertion of flow transfer operations on tensors:

%1 = flow.tensor.transfer %0 : tensor<16x16xi8> to #hal.device.affinity<@device_b>

these sub-sequently lower to async transfer in the stream dialect:

stream.async.transfer %src : ... -> to(#hal.device.affinity<@device_b>) ...

In the lowering from stream async to stream cmd this then lowers outgoing transfers to (

applyAsyncTransferOp(IREE::Stream::AffinityAttr executionAffinityAttr,
)

stream.cmd.copy %src[...], %dst[...], %length : ...
stream.cmd.flush to(#hal.device.affinity<@device_b>) %dst[...] : ...

incoming transfers on the other hand lower to: stream.cmd.invalidate and stream.cmd.copy

Missing lowering for stream.cmd.flush and stream.cmd.invalidate

The first gap in the lowering is that lowering of stream.cmd.flush is simply not implemented. The operations is simply erased in the current lowering. See

struct CmdFlushOpPattern : StreamConversionPattern<IREE::Stream::CmdFlushOp> {

Not only is the lowering of the operation missing also its implementation in the HAL command buffer is not present. The lowering to be implemented should follow what is implemented for the IREE::Stream::CmdCopyOp

struct CmdCopyOpPattern : StreamConversionPattern<IREE::Stream::CmdCopyOp> {
. This lowers to IREE::HAL::CommandBufferCopyBufferOp which is sub-sequently lowered to the VM call here

It also requires the extension of the HAL with the correct command buffer handler. See handling for hal.command_buffer.copy_buffer it should be analogous.

Similar for stream.cmd.invalidate

Note that the HAL implementation already provides functions for buffer flush and invalidation:

  • iree_hal_buffer_mapping_flush_range
  • iree_hal_buffer_mapping_invalidate_range

Copy operation when lowering

The current lowering can produce a copy of the buffer which breaks a true zero-copy path:

stream.cmd.copy %src[...], %dst[...], %length : ...
stream.cmd.flush to(#hal.device.affinity<@device_b>) %dst[...] : ...

Transfers can be eliminated when resource lifetimes match and the affinity/topology checks permit it. This replaces the transfer result with its source.

IREE::Stream::AsyncTransferOp transferOp, ElisionAnalysis &analysis,
For non-coherent memory this elision is not valid.

For true zero-copy we need to avoid the introduction of stream.cmd.copy.

To achieve this we should allow to communicate that no explicit buffer allocation and copy is required for flow.tensor.transfer and stream.async.transfer. Either derived from the device affinity or from an explicit attribute set on the operation. Zero-copy requires that the backing allocation is accessible to both devices and satisfies their usage requirements. Device affinity or an operation attribute must convey or establish this compatibility, shared physical memory alone is insufficient.

This should control the lowering of stream.async.transfer to avoid the insertion of the stream.cmd.copy

Note that simply leaving out the copy is not possible, there also has to be change in allocation so that the consumers read from the correct buffer. This needs to be handled in

allocateExecutionRegion(IREE::Stream::AsyncExecuteOp executeOp,

Flush and invalidate operations must have execution-time ordering semantics. A flush must execute after the writes it makes available, and completion must be included in the handoff’s signaled timepoint/fence. An invalidate must execute after the corresponding producer completion and before dependent reads. Implementations must not perform cache maintenance merely during command-buffer recording. Backends may satisfy these requirements through queued commands, ordered host work, or equivalent synchronization guarantees.

Invalidate/flush is ignored for single copy/fill

The current matching for transfer operations is ignoring flush/invalidate

This optimization must preserve required flush/invalidate operations or decline the optimization. A region containing only cache maintenance after copy removal must retain its execution and synchronization semantics.

Cases to be handled

  • Coherent shared memory: shared backing allocation, no copy, preserved ordering.
  • Non-coherent shared memory: shared backing allocation, no copy, correctly ordered flush/invalidate as required in each direction.
  • Incompatible or discrete memory: retain allocation and copy.
  • Multiple consumers and in-place updates: preserve tensor semantics and allocation lifetime.

What component(s) does this issue relate to?

No response

Additional context

No response

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions