Skip to content

Add accumulate method for PortChannel - #784

Open
Changho Hwang (chhwang) wants to merge 9 commits into
fix/ethernet-receiver-close-racefrom
chhwang/new-atomic-add
Open

Add accumulate method for PortChannel#784
Changho Hwang (chhwang) wants to merge 9 commits into
fix/ethernet-receiver-close-racefrom
chhwang/new-atomic-add

Conversation

@chhwang

@chhwang Changho Hwang (chhwang) commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds Connection::accumulate() and PortChannel::accumulate() for remote signed 64-bit addition. Callers provide only their contribution, allowing concurrent writers to share a counter.

The operation uses the TriggerAccumulate FIFO opcode. Its operand occupies the trigger's first word. FIFO readiness comes from lap-parity commit bits, so zero-valued operands are valid.

Transport support

Transport Support
IB NIC atomic fetch-and-add; rejected in no-atomic mode
Ethernet Receive-side read-modify-write, serialized across connections
CudaIpc on ROCm Atomic kernel on the connection stream
CudaIpc on CUDA Rejected; no safe, practical proxy-side RMW

Validation and ordering

  • Accumulate targets must contain a naturally aligned 8-byte word at the requested offset.
  • Bounds checks are overflow-safe and run before issuing transport work.
  • Accumulate is asynchronous. A later signal or flush on the same connection preserves the connection's operation ordering.
  • Packed trigger fields reuse the existing debug-only checked constructor without adding release-mode validation branches.

Tests

Coverage includes:

  • positive, negative, and zero operands;
  • concurrent and multi-rank fan-in;
  • accumulate-before-signal/flush ordering;
  • unsupported CUDA CudaIpc and IB no-atomic modes;
  • undersized, exact-end, partial-word, huge-offset, and misaligned targets;
  • collective capability/topology gating for IB and CudaIpc tests.

Follow-up validation for the current head:

  • 8× A100, CUDA 12.9: consolidated one-to-one accumulate 4/4 passed; fan-in 2/2 passed.
  • 8× MI300X, ROCm 6.2.4: consolidated accumulate 7/7 passed; full 8-rank non-IB aggregate 30 passed / 8 expected skips.

The original implementation was also validated on H200/CUDA 12.9 and MI300X/ROCm 7.2.

@chhwang Changho Hwang (chhwang) changed the title Add atomicAdd method for PortChannel Add accumulate method for PortChannel Aug 4, 2026
@chhwang
Changho Hwang (chhwang) marked this pull request as ready for review August 4, 2026 23:36
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@chhwang
Changho Hwang (chhwang) force-pushed the chhwang/new-atomic-add branch 3 times, most recently from ccfecde to 36ce485 Compare August 6, 2026 08:22
@chhwang
Changho Hwang (chhwang) changed the base branch from main to chhwang/enhanced-fifo August 7, 2026 19:07
Base automatically changed from chhwang/enhanced-fifo to main August 12, 2026 23:10
`TriggerData`, `TriggerFlag`, and `TriggerSync` were combinable bits, but the
device API only ever produced five of the seven combinations, and the proxy
tested them with three independent masks. Replace them with one opcode per
operation the API can express:

    TriggerNone = 0   TriggerPut = 1                TriggerSignal = 2
    TriggerFlush = 3  TriggerPutWithSignal = 4      TriggerPutWithSignalAndFlush = 5

Seven values fit the existing 3-bit field with one spare. `handleTrigger`
becomes a switch that names one operation per case and warns on anything
unknown, so a combination nothing emits is now unrepresentable, and a trigger
whose type field is unset is no longer a valid operation.

The old names are removed rather than redefined. Code that composed them with
`|`, or tested them with `&`, must fail to compile: `TriggerFlush` is 3, which
has the old `TriggerData` bit set, so a mask test would silently misbehave.

No behavior change. mp_unit_tests 29/29 and unit_tests 34/34 on H200.
Add `Connection::accumulate()` and `PortChannel::accumulate()`, which add a
64-bit signed value to remote memory. Unlike `updateAndSync()`, the caller
needs to know only its own contribution, not the destination's current value.

Carried by the `TriggerAccumulate` opcode, with the operand in the trigger's
first word. FIFO readiness now comes from the lap-parity commit bit, so a zero
operand traverses the proxy like any other trigger; transport tests interleave
zero and nonzero accumulates to cover that path.

The addition has to be a real read-modify-write at the destination, which not
every transport can do:

- IB: any number of writers, via RDMA fetch-and-add. Rejected in no-atomic
  mode, where the device has no RDMA atomics.
- Ethernet: any number of remote writers. The receiving process performs the
  update, and its connections are serialized against each other; without that
  a 7-writer fan-in loses ~74% of updates.
- CudaIpc on ROCm: any number of writers. The proxy runs a kernel on the
  connection's stream, which also orders it against a following signal. A
  kernel there runs even while the caller's kernel spins.
- CudaIpc on CUDA: not supported, throws InvalidUsage. The host cannot
  read-modify-write device memory, and a proxy-launched kernel either cannot
  be scheduled while the caller's kernel spins (same context) or costs 2391 us
  per operation (separate context), against 19 us for a plain remote store.

Tested on H200 (CUDA 12.9) and MI300X (ROCm 7.2), including an 8-rank fan-in
test with 7 concurrent writers into one address.
@chhwang
Changho Hwang (chhwang) changed the base branch from main to fix/ethernet-receiver-close-race August 20, 2026 08:04
constexpr TriggerType TriggerFlush = 4; // Flush the connection.
constexpr TriggerType TriggerAccumulate = 6; // Add a value to remote memory.
constexpr TriggerType TriggerPutWithSignalAndFlush = 7; // Transfer data, signal, then flush.
// 5 is unassigned.

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.

Do we need to reserve 5 here? You mean user may build customized algo with old API?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's just unused so unassigned, but probably we'd better reserve for a future operation. Will do

// not wait for the caller.
accumulateU64Kernel<<<1, 1, 0, *stream_>>>(dst, value);
MSCCLPP_CUDATHROW(cudaGetLastError());
dirty_ = true;

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.

Why need to set dirty here? It will be reset some where?

Comment thread src/core/connection.cc

// Every peer terminates its socket in this process, so several recv threads can be here
// at once for one address. The read-modify-write below is not atomic, so serialize it
// against the other recv threads.

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.

Why there will be several threads here. For each device we just launch one recv thread, no?

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