200 lines
7.3 KiB
C++
200 lines
7.3 KiB
C++
// Copyright 2024 KVCache.AI
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// First-submit latency probe for EFA transport.
|
|
//
|
|
// transfer_engine_bench reports a 10-second throughput average, which
|
|
// hides the two costs that matter for first-request latency: the
|
|
// handshake / fi_av_insert that fires on the first send to each
|
|
// (local_NIC, peer_NIC) pair, and the optional warmupSegment() that
|
|
// pre-pays that cost outside the critical path. This probe times
|
|
// those two things separately so the cost of each is directly visible.
|
|
//
|
|
// Reports:
|
|
// - warmup: time for EfaTransport::warmupSegment() (if enabled)
|
|
// - submit #0..#N-1: time for a single 1 MB submitTransfer + poll
|
|
//
|
|
// The first submit carries the handshake cost when warmup is off; the
|
|
// rest are steady-state. It's a two-host tool (not hooked into ctest)
|
|
// used to:
|
|
// (a) decide whether to call warmupSegment() in your application —
|
|
// does it actually remove the stall?
|
|
// (b) compare this PR's shared-endpoint cost against upstream
|
|
// per-peer-endpoint cost (see the numbers in docs/.../efa_transport.md
|
|
// under "First-request latency").
|
|
//
|
|
// Usage:
|
|
// Target: ./efa_first_submit_probe --mode=target ...
|
|
// Initiator: ./efa_first_submit_probe --mode=initiator \
|
|
// --segment_id=<target_host>:<port> \
|
|
// --warmup=1 --iters=5
|
|
|
|
#include <gflags/gflags.h>
|
|
#include <glog/logging.h>
|
|
#include <numa.h>
|
|
|
|
#include <chrono>
|
|
#include <cstring>
|
|
#include <memory>
|
|
#include <string>
|
|
|
|
#include "transfer_engine.h"
|
|
#include "transport/efa_transport/efa_transport.h"
|
|
#include "transport/transport.h"
|
|
|
|
using namespace mooncake;
|
|
|
|
DEFINE_string(mode, "initiator", "initiator or target");
|
|
DEFINE_string(metadata_server, "P2PHANDSHAKE", "metadata backend");
|
|
DEFINE_string(local_server_name, "",
|
|
"local server name (host:port); default = hostname:12345");
|
|
DEFINE_string(
|
|
segment_id, "",
|
|
"target <host>:<port> from target's startup log (initiator only)");
|
|
DEFINE_bool(warmup, true, "call warmupSegment before first submit");
|
|
DEFINE_int32(iters, 5, "number of timed submits after warmup");
|
|
DEFINE_uint64(xfer_size, 1 << 20, "bytes per submit (default 1 MB)");
|
|
DEFINE_uint64(buffer_size, 1ULL << 30, "registered buffer size (default 1 GB)");
|
|
|
|
static std::string defaultLocalServerName() {
|
|
char host[256];
|
|
if (gethostname(host, sizeof(host)) != 0) return "127.0.0.1:12345";
|
|
return std::string(host) + ":12345";
|
|
}
|
|
|
|
static double toMs(std::chrono::steady_clock::duration d) {
|
|
return std::chrono::duration<double, std::milli>(d).count();
|
|
}
|
|
|
|
static int runTarget() {
|
|
auto engine = std::make_unique<TransferEngine>(false);
|
|
engine->getLocalTopology()->discover({});
|
|
|
|
std::string name = FLAGS_local_server_name.empty()
|
|
? defaultLocalServerName()
|
|
: FLAGS_local_server_name;
|
|
auto hp = parseHostNameWithPort(name);
|
|
int rc =
|
|
engine->init(FLAGS_metadata_server, name, hp.first.c_str(), hp.second);
|
|
LOG_ASSERT(!rc) << "engine init failed";
|
|
|
|
Transport *xport = engine->installTransport("efa", nullptr);
|
|
LOG_ASSERT(xport) << "installTransport(efa) failed";
|
|
|
|
void *buf = numa_alloc_onnode(FLAGS_buffer_size, 0);
|
|
LOG_ASSERT(buf) << "numa_alloc_onnode failed";
|
|
|
|
rc = engine->registerLocalMemory(buf, FLAGS_buffer_size, "cpu:0");
|
|
LOG_ASSERT(!rc) << "registerLocalMemory failed";
|
|
|
|
LOG(INFO) << "[target] ready, addr=" << engine->getLocalIpAndPort();
|
|
LOG(INFO) << "[target] Ctrl-C to stop";
|
|
|
|
pause(); // block until SIGINT
|
|
|
|
engine->unregisterLocalMemory(buf);
|
|
numa_free(buf, FLAGS_buffer_size);
|
|
return 0;
|
|
}
|
|
|
|
static int runInitiator() {
|
|
if (FLAGS_segment_id.empty()) {
|
|
LOG(ERROR) << "--segment_id is required for initiator";
|
|
return 1;
|
|
}
|
|
|
|
auto engine = std::make_unique<TransferEngine>(false);
|
|
engine->getLocalTopology()->discover({});
|
|
|
|
std::string name = FLAGS_local_server_name.empty()
|
|
? defaultLocalServerName()
|
|
: FLAGS_local_server_name;
|
|
auto hp = parseHostNameWithPort(name);
|
|
int rc =
|
|
engine->init(FLAGS_metadata_server, name, hp.first.c_str(), hp.second);
|
|
LOG_ASSERT(!rc) << "engine init failed";
|
|
|
|
Transport *xport = engine->installTransport("efa", nullptr);
|
|
LOG_ASSERT(xport) << "installTransport(efa) failed";
|
|
|
|
void *buf = numa_alloc_onnode(FLAGS_buffer_size, 0);
|
|
LOG_ASSERT(buf) << "numa_alloc_onnode failed";
|
|
memset(buf, 0xAB, FLAGS_xfer_size);
|
|
|
|
rc = engine->registerLocalMemory(buf, FLAGS_buffer_size, "cpu:0");
|
|
LOG_ASSERT(!rc) << "registerLocalMemory failed";
|
|
|
|
auto segment_id = engine->openSegment(FLAGS_segment_id);
|
|
LOG_ASSERT(segment_id != (SegmentID)-1) << "openSegment failed";
|
|
|
|
auto seg_desc = engine->getMetadata()->getSegmentDescByID(segment_id);
|
|
LOG_ASSERT(seg_desc) << "getSegmentDescByID failed";
|
|
uint64_t remote_base = (uint64_t)seg_desc->buffers[0].addr;
|
|
|
|
LOG(INFO) << "[initiator] peer=" << FLAGS_segment_id
|
|
<< " warmup=" << (FLAGS_warmup ? "ON" : "OFF")
|
|
<< " iters=" << FLAGS_iters << " xfer_size=" << FLAGS_xfer_size;
|
|
|
|
if (FLAGS_warmup) {
|
|
auto *efa = dynamic_cast<EfaTransport *>(xport);
|
|
LOG_ASSERT(efa) << "transport is not EfaTransport";
|
|
auto t0 = std::chrono::steady_clock::now();
|
|
int wrc = efa->warmupSegment(FLAGS_segment_id);
|
|
auto elapsed = std::chrono::steady_clock::now() - t0;
|
|
LOG(INFO) << "warmup: " << toMs(elapsed) << " ms (rc=" << wrc << ")";
|
|
}
|
|
|
|
for (int i = 0; i < FLAGS_iters; ++i) {
|
|
auto t0 = std::chrono::steady_clock::now();
|
|
|
|
auto batch_id = engine->allocateBatchID(1);
|
|
TransferRequest req;
|
|
req.opcode = TransferRequest::WRITE;
|
|
req.length = FLAGS_xfer_size;
|
|
req.source = (uint8_t *)buf;
|
|
req.target_id = segment_id;
|
|
req.target_offset = remote_base;
|
|
|
|
Status s = engine->submitTransfer(batch_id, {req});
|
|
LOG_ASSERT(s.ok()) << "submitTransfer #" << i
|
|
<< " failed: " << s.ToString();
|
|
|
|
TransferStatus status;
|
|
for (int poll = 0; poll < 20'000'000; ++poll) {
|
|
s = engine->getTransferStatus(batch_id, 0, status);
|
|
LOG_ASSERT(s.ok());
|
|
if (status.s == TransferStatusEnum::COMPLETED ||
|
|
status.s == TransferStatusEnum::FAILED)
|
|
break;
|
|
}
|
|
LOG_ASSERT(status.s == TransferStatusEnum::COMPLETED)
|
|
<< "submit #" << i << " did not complete";
|
|
|
|
engine->freeBatchID(batch_id);
|
|
|
|
auto elapsed = std::chrono::steady_clock::now() - t0;
|
|
LOG(INFO) << "submit #" << i << ": " << toMs(elapsed) << " ms";
|
|
}
|
|
|
|
engine->unregisterLocalMemory(buf);
|
|
numa_free(buf, FLAGS_buffer_size);
|
|
return 0;
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
gflags::ParseCommandLineFlags(&argc, &argv, false);
|
|
google::InitGoogleLogging(argv[0]);
|
|
FLAGS_logtostderr = 1;
|
|
|
|
if (FLAGS_mode == "target") return runTarget();
|
|
if (FLAGS_mode == "initiator") return runInitiator();
|
|
|
|
LOG(ERROR) << "--mode must be 'target' or 'initiator'";
|
|
return 1;
|
|
}
|