Skip to content

Commit 1d95a78

Browse files
Peng Chenmeta-codesync[bot]
authored andcommitted
comms/uniflow: instrument the receive path across the controller and the transport (meta-pytorch#3905)
Summary: Pull Request resolved: meta-pytorch#3905 Folded from two adjacent diffs; each half is stated separately below so the two arguments stay reviewable on their own terms. --- controller: recv header-wait and payload-drain timings (was D117632427) --- TcpConn::syncRecv() populates the existing RecvPhaseStats with the time spent waiting for the length prefix versus draining the payload, plus frame and byte counts. Splitting the two phases is what makes a slow receive interpretable: a large headerWaitNs means we were waiting on the peer, while a large payloadDrainNs means the socket itself was the limit. All four counters are relaxed fetch_add on atomics already declared in Controller.h, so this adds two steady_clock reads per frame and no synchronisation. --- transport: receive-slab hits, misses and vector receives (was D117632428) --- Adds receiveSlabAttempts_, receiveSlabMisses_, and vectorReceiveCount_ to TcpTransport and reports them on the existing tcp phases log line. A miss means the reader could not get a pinned slab and fell back to a vector-backed receive, which changes the H2D path for that frame, so distinguishing the two is necessary before drawing conclusions from an aggregate drain number. The counters are relaxed atomics incremented in readerLoop(). Differential Revision: D117632428
1 parent 6ee2c86 commit 1d95a78

5 files changed

Lines changed: 186 additions & 4 deletions

File tree

comms/uniflow/controller/TcpController.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,12 +613,16 @@ Result<size_t> TcpConn<IOPolicy>::syncRecv(std::span<uint8_t> buf) {
613613
return Err(ErrCode::NotConnected, "Socket is not connected");
614614
}
615615

616+
auto& stats = recvPhaseStats();
617+
const auto tStart = std::chrono::steady_clock::now();
618+
616619
uint32_t rawLen = 0;
617620
if (!recvAll(&rawLen, sizeof(rawLen))) {
618621
return Err(
619622
ErrCode::ConnectionFailed,
620623
"recv header failed: " + std::system_category().message(errno));
621624
}
625+
const auto tFirstByte = std::chrono::steady_clock::now();
622626

623627
uint32_t len = ntohl(rawLen);
624628
if (len > kMaxMessageSize) {
@@ -642,6 +646,17 @@ Result<size_t> TcpConn<IOPolicy>::syncRecv(std::span<uint8_t> buf) {
642646
"recv payload failed: " + std::system_category().message(errno));
643647
}
644648

649+
const auto tDone = std::chrono::steady_clock::now();
650+
using ns = std::chrono::nanoseconds;
651+
stats.headerWaitNs.fetch_add(
652+
std::chrono::duration_cast<ns>(tFirstByte - tStart).count(),
653+
std::memory_order_relaxed);
654+
stats.payloadDrainNs.fetch_add(
655+
std::chrono::duration_cast<ns>(tDone - tFirstByte).count(),
656+
std::memory_order_relaxed);
657+
stats.frames.fetch_add(1, std::memory_order_relaxed);
658+
stats.payloadBytes.fetch_add(len, std::memory_order_relaxed);
659+
645660
UNIFLOW_LOG_DEBUG("TcpConn::recv(span): fd={} bytes={}", sock_, len);
646661
return static_cast<size_t>(len);
647662
}

comms/uniflow/controller/tests/TcpConnTest.cpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,31 @@ TEST_P(TcpConnTest, SendRecvBidirectional) {
120120
EXPECT_TRUE(recv3.empty());
121121
}
122122

123+
TEST_P(TcpConnTest, SpanRecvRecordsPhaseStats) {
124+
auto [clientConn, serverConn] = connectPair();
125+
ASSERT_NE(clientConn, nullptr);
126+
ASSERT_NE(serverConn, nullptr);
127+
128+
std::vector<uint8_t> sent(4096, 0xA5);
129+
std::vector<uint8_t> received(sent.size());
130+
ASSERT_TRUE(clientConn->send(sent).get().hasValue());
131+
132+
auto recvResult = serverConn->recv(std::span<uint8_t>(received)).get();
133+
ASSERT_TRUE(recvResult.hasValue()) << recvResult.error().toString();
134+
EXPECT_EQ(recvResult.value(), sent.size());
135+
EXPECT_EQ(received, sent);
136+
137+
auto& stats = serverConn->recvPhaseStats();
138+
EXPECT_EQ(stats.frames.load(std::memory_order_relaxed), 1);
139+
EXPECT_EQ(stats.payloadBytes.load(std::memory_order_relaxed), sent.size());
140+
141+
stats.reset();
142+
EXPECT_EQ(stats.headerWaitNs.load(std::memory_order_relaxed), 0);
143+
EXPECT_EQ(stats.payloadDrainNs.load(std::memory_order_relaxed), 0);
144+
EXPECT_EQ(stats.frames.load(std::memory_order_relaxed), 0);
145+
EXPECT_EQ(stats.payloadBytes.load(std::memory_order_relaxed), 0);
146+
}
147+
123148
TEST_P(TcpConnTest, SendRecvLargeData) {
124149
auto [clientConn, serverConn] = connectPair();
125150
ASSERT_NE(clientConn, nullptr);

comms/uniflow/transport/tcp/TcpTransport.cpp

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1527,6 +1527,12 @@ void TcpTransport::logAndResetPhaseStats(std::string_view label) {
15271527
h2dState_->copyNs.load(std::memory_order_relaxed);
15281528
const uint64_t copies = dstCopyCount_.load(std::memory_order_relaxed) +
15291529
h2dState_->copyCount.load(std::memory_order_relaxed);
1530+
const uint64_t slabAttempts =
1531+
receiveSlabAttempts_.load(std::memory_order_relaxed);
1532+
const uint64_t slabMisses =
1533+
receiveSlabMisses_.load(std::memory_order_relaxed);
1534+
const uint64_t vectorReceives =
1535+
vectorReceiveCount_.load(std::memory_order_relaxed);
15301536

15311537
if (frames == 0) {
15321538
UNIFLOW_LOG_INFO("tcp phases [{}]: no frames", label);
@@ -1546,7 +1552,8 @@ void TcpTransport::logAndResetPhaseStats(std::string_view label) {
15461552
UNIFLOW_LOG_INFO(
15471553
"tcp phases [{}]: frames={} bytes={} | first-byte {:.1f}us/frame "
15481554
"({:.1f}%) | drain {:.1f}us/frame ({:.1f}%, {:.2f} GB/s) | dstcopy "
1549-
"{:.1f}us x{} ({:.1f}% of wire)",
1555+
"{:.1f}us x{} ({:.1f}% of wire) | receive_slabs attempts={} misses={} "
1556+
"vector_recvs={}",
15501557
label,
15511558
frames,
15521559
bytes,
@@ -1557,13 +1564,19 @@ void TcpTransport::logAndResetPhaseStats(std::string_view label) {
15571564
drainGBps,
15581565
copies > 0 ? static_cast<double>(copyNs) / copies / 1000.0 : 0.0,
15591566
copies,
1560-
pct(copyNs));
1567+
pct(copyNs),
1568+
slabAttempts,
1569+
slabMisses,
1570+
vectorReceives);
15611571
}
15621572
rs.reset();
15631573
dstCopyNs_.store(0, std::memory_order_relaxed);
15641574
dstCopyCount_.store(0, std::memory_order_relaxed);
15651575
h2dState_->copyNs.store(0, std::memory_order_relaxed);
15661576
h2dState_->copyCount.store(0, std::memory_order_relaxed);
1577+
receiveSlabAttempts_.store(0, std::memory_order_relaxed);
1578+
receiveSlabMisses_.store(0, std::memory_order_relaxed);
1579+
vectorReceiveCount_.store(0, std::memory_order_relaxed);
15671580
}
15681581

15691582
void TcpTransport::readerLoop() noexcept {
@@ -1576,7 +1589,11 @@ void TcpTransport::readerLoop() noexcept {
15761589
TcpPinnedSlab receiveSlab;
15771590
if (config_.asyncGetH2d) {
15781591
if (auto pool = receivePoolIfCreated()) {
1592+
receiveSlabAttempts_.fetch_add(1, std::memory_order_relaxed);
15791593
receiveSlab = pool->tryAcquire(/*allowReserved=*/true);
1594+
if (!receiveSlab) {
1595+
receiveSlabMisses_.fetch_add(1, std::memory_order_relaxed);
1596+
}
15801597
}
15811598
}
15821599
if (receiveSlab) {
@@ -1596,6 +1613,7 @@ void TcpTransport::readerLoop() noexcept {
15961613
continue;
15971614
}
15981615

1616+
vectorReceiveCount_.fetch_add(1, std::memory_order_relaxed);
15991617
auto result = dataConn_->recv(msg).get();
16001618
if (!result) {
16011619
// Connection closed, errored, or idle-timed-out; stop reading.
@@ -2148,7 +2166,7 @@ std::future<Status> TcpTransport::recv(
21482166
}
21492167

21502168
void TcpTransport::shutdown() {
2151-
std::lock_guard<std::mutex> lk(lifecycleMu_);
2169+
std::lock_guard<std::mutex> lifecycleLock(lifecycleMu_);
21522170
// One-shot, but checked under the mutex rather than before taking it:
21532171
// shutdown() is called twice in the normal flow (MultiTransport::shutdown()
21542172
// then ~TcpTransport), and the second caller must not return while the first
@@ -2159,7 +2177,7 @@ void TcpTransport::shutdown() {
21592177
running_.store(false, std::memory_order_release);
21602178

21612179
{
2162-
std::lock_guard<std::mutex> lk(outMu_);
2180+
std::lock_guard<std::mutex> outLock(outMu_);
21632181
outClosed_ = true;
21642182
}
21652183
outCv_.notify_all();

comms/uniflow/transport/tcp/TcpTransport.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,9 @@ class TcpTransport : public Transport {
848848
// nothing more.
849849
std::atomic<uint64_t> dstCopyNs_{0};
850850
std::atomic<uint64_t> dstCopyCount_{0};
851+
std::atomic<uint64_t> receiveSlabAttempts_{0};
852+
std::atomic<uint64_t> receiveSlabMisses_{0};
853+
std::atomic<uint64_t> vectorReceiveCount_{0};
851854
std::shared_ptr<H2dPollState> h2dState_;
852855

853856
// Serialises bind()/connect()/shutdown(), which together own server_,

comms/uniflow/transport/tcp/tests/unit/TcpReceivePoolTest.cpp

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,127 @@ TEST_F(TcpReceivePoolTest, VramReadReplyKeepsSlabUntilEventCompletes) {
367367
EXPECT_TRUE(pool->tryAcquire(/*allowReserved=*/true));
368368
}
369369

370+
TEST_F(
371+
TcpReceivePoolTest,
372+
EventRecordFailureCompletesGetWhenStreamSynchronizationSucceeds) {
373+
makeTransport();
374+
auto pool = createReceivePool();
375+
ASSERT_NE(pool, nullptr);
376+
auto slab = pool->tryAcquire(/*allowReserved=*/true);
377+
ASSERT_TRUE(slab);
378+
auto frame = readReplyFrame(/*reqId=*/1, kLen);
379+
ASSERT_LE(frame.size(), slab.capacity());
380+
std::memcpy(slab.data(), frame.data(), frame.size());
381+
const auto bytes = std::span<const uint8_t>{slab.data(), frame.size()};
382+
383+
std::vector<uint8_t> destination(kLen);
384+
auto* const callerStream = reinterpret_cast<void*>(0xCA11);
385+
auto future = postVramRead(destination.data(), callerStream);
386+
387+
EXPECT_CALL(
388+
*cudaApi_,
389+
memcpyAsync(
390+
destination.data(),
391+
::testing::_,
392+
kLen,
393+
kMockMemcpyH2D,
394+
static_cast<MockStream>(callerStream)))
395+
.WillOnce([&](void* dst, const void* src, size_t len, auto, auto) {
396+
std::memcpy(dst, src, len);
397+
return Ok();
398+
});
399+
EXPECT_CALL(*cudaApi_, eventCreate(::testing::_))
400+
.WillOnce([](auto* event) -> Status {
401+
*event = {};
402+
return Ok();
403+
});
404+
EXPECT_CALL(
405+
*cudaApi_,
406+
eventRecord(::testing::_, static_cast<MockStream>(callerStream)))
407+
.WillOnce(
408+
::testing::Return(
409+
Err(ErrCode::DriverError, "test: event record failed")));
410+
EXPECT_CALL(
411+
*cudaApi_, streamSynchronize(static_cast<MockStream>(callerStream)))
412+
.WillOnce(::testing::Return(Ok()));
413+
EXPECT_CALL(*cudaApi_, eventDestroy(::testing::_))
414+
.WillOnce(::testing::Return(Ok()));
415+
EXPECT_CALL(*cudaApi_, eventQuery(::testing::_)).Times(0);
416+
417+
handleFrame(bytes, std::move(slab));
418+
419+
ASSERT_EQ(
420+
future.wait_for(std::chrono::seconds(5)), std::future_status::ready);
421+
EXPECT_TRUE(future.get().hasValue());
422+
EXPECT_TRUE(
423+
std::all_of(destination.begin(), destination.end(), [](uint8_t b) {
424+
return b == uint8_t{0xCD};
425+
}));
426+
auto returnedSlabs = pool->acquire(2);
427+
EXPECT_TRUE(returnedSlabs.hasValue());
428+
}
429+
430+
TEST_F(
431+
TcpReceivePoolTest,
432+
EventQueryFailureCompletesGetWhenStreamSynchronizationSucceeds) {
433+
makeTransport();
434+
auto pool = createReceivePool();
435+
ASSERT_NE(pool, nullptr);
436+
auto slab = pool->tryAcquire(/*allowReserved=*/true);
437+
ASSERT_TRUE(slab);
438+
auto frame = readReplyFrame(/*reqId=*/1, kLen);
439+
ASSERT_LE(frame.size(), slab.capacity());
440+
std::memcpy(slab.data(), frame.data(), frame.size());
441+
const auto bytes = std::span<const uint8_t>{slab.data(), frame.size()};
442+
443+
std::vector<uint8_t> destination(kLen);
444+
auto* const callerStream = reinterpret_cast<void*>(0xCA11);
445+
auto future = postVramRead(destination.data(), callerStream);
446+
447+
EXPECT_CALL(
448+
*cudaApi_,
449+
memcpyAsync(
450+
destination.data(),
451+
::testing::_,
452+
kLen,
453+
kMockMemcpyH2D,
454+
static_cast<MockStream>(callerStream)))
455+
.WillOnce([&](void* dst, const void* src, size_t len, auto, auto) {
456+
std::memcpy(dst, src, len);
457+
return Ok();
458+
});
459+
EXPECT_CALL(*cudaApi_, eventCreate(::testing::_))
460+
.WillOnce([](auto* event) -> Status {
461+
*event = {};
462+
return Ok();
463+
});
464+
EXPECT_CALL(
465+
*cudaApi_,
466+
eventRecord(::testing::_, static_cast<MockStream>(callerStream)))
467+
.WillOnce(::testing::Return(Ok()));
468+
EXPECT_CALL(*cudaApi_, eventQuery(::testing::_))
469+
.WillOnce([](auto) -> Result<bool> {
470+
return Err(ErrCode::DriverError, "test: event query failed");
471+
});
472+
EXPECT_CALL(
473+
*cudaApi_, streamSynchronize(static_cast<MockStream>(callerStream)))
474+
.WillOnce(::testing::Return(Ok()));
475+
EXPECT_CALL(*cudaApi_, eventDestroy(::testing::_))
476+
.WillOnce(::testing::Return(Ok()));
477+
478+
handleFrame(bytes, std::move(slab));
479+
480+
ASSERT_EQ(
481+
future.wait_for(std::chrono::seconds(5)), std::future_status::ready);
482+
EXPECT_TRUE(future.get().hasValue());
483+
EXPECT_TRUE(
484+
std::all_of(destination.begin(), destination.end(), [](uint8_t b) {
485+
return b == uint8_t{0xCD};
486+
}));
487+
auto returnedSlabs = pool->acquire(2);
488+
EXPECT_TRUE(returnedSlabs.hasValue());
489+
}
490+
370491
TEST_F(TcpReceivePoolTest, VectorBackedVramReadReplyRemainsSynchronous) {
371492
makeTransport();
372493
auto frame = readReplyFrame(/*reqId=*/1, kLen);

0 commit comments

Comments
 (0)