RCCL Replayer update (#1603)

RCCL recorder w/ suggested change and UT



[ROCm/rccl commit: 9a55ff60a9]
This commit is contained in:
Tim
2025-04-19 00:21:27 -04:00
committed by GitHub
szülő de2b66921a
commit 58ee618194
18 fájl változott, egészen pontosan 1123 új sor hozzáadva és 93 régi sor törölve
@@ -0,0 +1,669 @@
#include "bootstrap.h"
#include "group.h"
#include "utils.h"
#include <cstring>
#include <string>
#include <iomanip>
#include <sys/syscall.h>
using namespace std::chrono;
namespace rccl
{
__thread int Recorder::rcclReplayThreadIdx = -1;
int Recorder::depth = 0;
static char buffer[4096];
static rcclCall_t lastcall = rrGroupStart; // for trailing comma, need modify for multithread case for Bcast
void indent(int depth, std::ofstream& o) {for (int i = 0; i < depth; i++) o << " ";}
void newLine(std::ofstream& o) {if (lastcall != rrGroupStart) o << ","; o << std::endl;}
static uint64_t hashUniqueId(ncclUniqueId const &id) {
char const *bytes = (char const*)&id;
uint64_t h = 0xdeadbeef;
for(int i=0; i < (int)sizeof(ncclUniqueId); i++) {
h ^= h >> 32;
h *= 0x8db3db47fa2994ad;
h += bytes[i];
}
return h;
}
rcclApiCall::rcclApiCall(rcclCall_t type, const ncclInfo& info)://name(rcclCallStr[call.type]), // opName
type(type),
opCount(info.comm->opCount),
sendbuff(info.sendbuff),
recvbuff(info.recvbuff),
count(info.count),
datatype(info.datatype),
op(info.op),
root(info.root),
comm(info.comm),
nRanks(info.comm->nRanks),
stream(info.stream),
nTasks(info.comm->planner.nTasksP2p + info.comm->planner.nTasksColl),
globalRank(info.comm->localRankToRank[info.comm->localRank]){}
rcclApiCall::rcclApiCall(rcclCall_t type) : type(type){}
std::string siminfo_fmt = "[size : %zu, magic : %u, version : %u, estimated time : %f, timestamp : %f]";
std::string config_fmt = ", ncclConfig : [size : %zu, magic : %u, version : %u, blocking : %d, cgaClusterSize : %d, minCTA : %d, maxCTA : %d, netname : %s, splitshare : %d]";
std::string ctxt_fmt = "time : %lf, thread : %d, device : %d, captured : %d, graphID : %llu ]]"; // implicit context info
std::string ubr_fmt = "%s : [comm : %p, buff : %p, returned handle : %p, size : %zu, context : [";
std::string getId_fmt = "%s : [uniqueID : %llu, context : [";
std::string ubDereg_fmt = "%s : [comm : %p, handle : %p, context : [";
std::string rank_fmt = "%s : [size : %d, uniqueID : %llu, rank : %d, context : [";
std::string init_fmt = "%s : [comm : %p, size : %d, uniqueID : %llu, rank : %d, dev : %d, context : [";
std::string all_fmt = "%s : [# of device : %d, context : [";
std::string destroy_fmt = "%s : [comm : %p, context : [";
std::string split_fmt = "%s : [comm : %p, color : %d, key : %d, newcomm : %p, context : [";
std::string alloc_fmt = "%s : [returned ptr : %p, size : %zu, context : [";
std::string free_fmt = "%s : [ptr : %p, context : [";
std::string redop_fmt = "%s : [scalar : %p, datatype : %d, op : %d, residence : %d, comm : %p, context : [";
std::string redopdestroy_fmt = "%s : [op : %d, comm : %p, context : [";
std::string coll_fmt = "%s : [opCount : %lx, sendbuff : %p, recvbuff : %p, count : %zu, datatype : %d, op : %d, root : %d, comm : %p, nranks : %d, stream : %p, task : %d, globalrank : %d, context : [";
Recorder::Recorder()
{
filename = getenv("RCCL_REPLAY_FILE") ? getenv("RCCL_REPLAY_FILE") : "";
if (!filename.size())
{
return;
}
logLevel = getenv("RCCL_LOG_LEVEL") ? std::stoi(getenv("RCCL_LOG_LEVEL")) : 1;
char hostname[1024];
getHostName(hostname, 1024, '.');
pid = getpid();
output_json = 0;
size_t dot;
std::string output_name, output_extension;
if ((dot = std::string(filename).find(".")) != std::string::npos)
{
output_name = std::string(filename).substr(0, dot);
output_extension = std::string(filename).substr(dot);
if (output_extension.compare(".json") == 0)
{
output_json = 1;
}
} else {
output_name = std::string(filename);
}
outputFile.open(output_name + std::to_string(pid) + output_extension,
output_json ? std::ofstream::out : std::ofstream::binary);
if (output_json)
{
outputFile << "{" << std::endl;
indent(2, outputFile);
outputFile << "hostname : " << hostname << ", version : 0,";
}
}
Recorder& Recorder::instance()
{
static Recorder _instance;
return _instance;
}
void Recorder::captureGpuContext(rcclApiCall& call) const
{
call.timestamp = duration_cast<duration<double>>(high_resolution_clock::now().time_since_epoch()).count() * 1000;
if (rcclReplayThreadIdx == -1)
{
rcclReplayThreadIdx = syscall(SYS_gettid);
}
int hipDev;
hipGetDevice(&hipDev); // need later change to copy from comm
call.pid = pid;
call.tid = rcclReplayThreadIdx;
call.hipDev = hipDev;
return;
}
// for single process use only, for now
// TODO: potentially need async logging for performance
void Recorder::write(const rcclApiCall &call)
{
if (!filename.size())
{
return ;
}
std::unique_lock<std::mutex> lock(writemtx);
if (lastcall == rrBcast && call.type == rrBroadcast)
{
return;
}
int len = -1;
if (output_json)
{
if (call.type == rrGroupEnd || call.type == rrGroupSimulatedEnd)
{
depth--;
outputFile << std::endl;
indent(2 + 2 * depth, outputFile);
outputFile << "}";
return ;
}
newLine(outputFile);
indent(2 + 2 * depth, outputFile);
lastcall = call.type;
switch (call.type) {
case rrGroupStart:
{
outputFile << "{";
depth++;
return ;
}
case rrCommRegister:
{
len = snprintf(buffer, 4096, ubr_fmt.c_str(),
rcclCallStr[call.type], call.comm, call.sendbuff, call.recvbuff, call.count);
break;
}
case rrCommDeregister:
{
len = snprintf(buffer, 4096, ubDereg_fmt.c_str(),
rcclCallStr[call.type], call.comm, call.recvbuff);
break;
}
case rrGetUniqueId:
{
len = snprintf(buffer, 4096, getId_fmt.c_str(), rcclCallStr[call.type], call.commId);
break;
}
case rrCommInitDev:
{
len = snprintf(buffer, 4096, init_fmt.c_str(), rcclCallStr[call.type], call.comm, call.nRanks, call.commId, call.globalRank, call.root);
break;
}
case rrCommInitRank:
case rrCommInitRankConfig:
{
len = snprintf(buffer, 4096, rank_fmt.c_str(), rcclCallStr[call.type], call.nRanks, call.commId, call.globalRank);
break; // detail to be provided by init dev or config info
}
case rrCommInitAll:
{
len = snprintf(buffer, 4096, all_fmt.c_str(), rcclCallStr[call.type], call.root);
break;
}
case rrCommFinalize:
case rrCommDestroy:
case rrCommAbort:
{
len = snprintf(buffer, 4096, destroy_fmt.c_str(), rcclCallStr[call.type], call.comm);
break;
}
case rrCommSplit:
{
len = snprintf(buffer, 4096, split_fmt.c_str(),
rcclCallStr[call.type], (void*)(call.commId), call.comm, call.nRanks, call.globalRank, call.comm);
break;
}
case rrMemAlloc:
{
len = snprintf(buffer, 4096, alloc_fmt.c_str(),
rcclCallStr[call.type], call.recvbuff, call.count);
break;
}
case rrMemFree:
{
len = snprintf(buffer, 4096, free_fmt.c_str(),
rcclCallStr[call.type], call.recvbuff);
break;
}
case rrRedOpCreatePreMulSum:
{
len = snprintf(buffer, 4096, redop_fmt.c_str(),
rcclCallStr[call.type], call.sendbuff, call.datatype, call.op, call.root, call.comm);
break;
}
case rrRedOpDestroy:
{
len = snprintf(buffer, 4096, redopdestroy_fmt.c_str(),
rcclCallStr[call.type], call.op, call.comm);
break;
}
default: // collectives
len = snprintf(buffer, 4096, coll_fmt.c_str(),
rcclCallStr[call.type], call.opCount, call.sendbuff, call.recvbuff, call.count, call.datatype,
call.op, call.root, call.comm, call.nRanks, call.stream, call.nTasks, call.globalRank);
}
outputFile.write(buffer, len);
len = snprintf(buffer, 4096, ctxt_fmt.c_str(), call.timestamp, call.tid, call.hipDev, call.graphCaptured, call.graphID);
outputFile.write(buffer, len);
} else {
outputFile.write((char*)&call, sizeof(rcclApiCall));
outputFile << std::endl;
}
outputFile.flush();
return ;
}
// wrapper function for graph launch callback
void Recorder::recordLater(void* idx)
{
Recorder& recorder = Recorder::instance();
size_t callidx = (size_t) idx;
rcclApiCall call = recorder.calls[callidx];
recorder.record(call);
}
void Recorder::record(const char* name)
{
if (!filename.size() || logLevel <= 1)
{
return;
}
double ts = duration_cast<duration<double>>(high_resolution_clock::now().time_since_epoch()).count() * 1000;
if (output_json) // will not record for binary replay export
{
std::unique_lock<std::mutex> lock(writemtx);
newLine(outputFile); lastcall = rrOtherCall;
indent(2 + 2 * depth, outputFile);
outputFile << name << " : [time : " << std::fixed << std::setprecision(6) << ts << "]";
}
//numCall++;
outputFile.flush();
}
ncclResult_t Recorder::record(rcclApiCall& call)
{
ncclResult_t ret = ncclSuccess;
captureGpuContext(call);
switch (call.type) {
case rrGroupStart:
case rrGroupEnd:
case rrGroupSimulatedEnd:
case rrGetUniqueId:
case rrCommInitDev:
case rrCommInitRank:
case rrCommInitAll:
case rrCommInitRankConfig:
case rrCommSplit:
case rrCommFinalize:
case rrCommDestroy:
case rrCommAbort: // communicator ops just exit and write
case rrCommRegister: // same with UBR
case rrCommDeregister:
case rrMemAlloc:
case rrMemFree:
case rrRedOpCreatePreMulSum:
case rrRedOpDestroy:
break;
// collectives, which may be registered with graph
case rrAllToAll: // should work with rest of the coll with nested sendrecv
default: // collective other than a2a/a2av
#if ROCM_VERSION >= 60100
hipStreamCaptureStatus status;
hipGraph_t graphCaptured;
unsigned long long graphID = 0;
CUDACHECK(hipStreamGetCaptureInfo_v2(call.stream, &status, &(call.graphID), &graphCaptured)); // shouldnt we need dependency?
if (status == hipStreamCaptureStatusActive) // when graph launched this should be disabled
{
call.graphCaptured = 1;
calls.push_back(call);
hipGraphNode_t logNode;
hipHostNodeParams p;
p.fn = &(Recorder::recordLater);
p.userData = (void*) (calls.size() - 1);
CUDACHECK(hipGraphAddHostNode(&logNode, graphCaptured, nullptr, 0, &p));
} else {
call.graphCaptured = 0;
}
#endif
}
write(call); // write immediatelly
return ret;
}
ncclResult_t Recorder::record(rcclCall_t type, const ncclInfo& info)
{
if (!filename.size())
{
return ncclSuccess;
}
rcclApiCall call(type, info);
return record(call);
}
ncclResult_t Recorder::record(rcclCall_t type, const void* sendbuff, void* recvbuff,
size_t count, ncclDataType_t datatype, ncclComm_t comm, hipStream_t stream, int root,
const size_t sendcounts[], const size_t sdispls[], const size_t recvcounts[], const size_t rdispls[])
{
if (!filename.size())
{
return ncclSuccess;
}
rcclApiCall call(type, {.sendbuff = sendbuff, .recvbuff = recvbuff, .count = count,
.datatype = datatype, .comm = comm, .stream = stream});
if (root != -1)
{
call.root = root;
}
ncclResult_t ret = record(call);
if (type == rrAllToAllv)
{
if (output_json)
{
int size = call.nRanks - 1;
outputFile << ", sendcounts : [";
for (int i = 0; i < size; i++) outputFile << sendcounts[i] << ", ";
outputFile << sendcounts[size] << "], sdispls : [";
for (int i = 0; i < size; i++) outputFile << sdispls[i] << ", ";
outputFile << sdispls[size] << "], recvcounts : [";
for (int i = 0; i < size; i++) outputFile << recvcounts[i] << ", ";
outputFile << recvcounts[size] << "], rdispls : [";
for (int i = 0; i < size; i++) outputFile << rdispls[i] << ", ";
outputFile << rdispls[size] << "]";
outputFile.flush();
}
// else export to binary
}
return ret;
}
ncclResult_t Recorder::record(rcclCall_t type, ncclRedOp_t op, ncclComm_t comm, ncclDataType_t datatype, ncclScalarResidence_t residence, void* scalar)
{
if (!filename.size())
{
return ncclSuccess;
}
rcclApiCall call(type, {.op = op, .comm = comm});
if (type == rrRedOpCreatePreMulSum)
{
call.sendbuff = scalar;
call.datatype = datatype;
call.root = residence;
call.sendbuff = scalar;
}
return record(call);
//TODO: printout scalar
}
ncclResult_t Recorder::record(rcclCall_t type, int groupDepth)
{
if (!filename.size())
{
return ncclSuccess;
}
rcclApiCall gc(type);
gc.groupDepth = groupDepth;
return record(gc);
}
ncclResult_t Recorder::record(rcclCall_t type, int size, int rank, ncclUniqueId* commId, ncclComm_t comm, int device)
{
if (!filename.size())
{
return ncclSuccess;
}
rcclApiCall initCall(type);
if (type == rrCommSplit)
{
initCall.comm = comm;
initCall.commId = (uint64_t)commId;
} else {
initCall.commId = hashUniqueId(*commId);
}
initCall.nRanks = size;
initCall.globalRank = rank;
if (type == rrCommInitDev)
{
initCall.root = device;
initCall.comm = comm;
}
return record(initCall);
}
// comm destroy
ncclResult_t Recorder::record(rcclCall_t type, ncclComm_t comm)
{
if (!filename.size())
{
return ncclSuccess;
}
rcclApiCall call(type);
call.comm = comm;
return record(call);
}
ncclResult_t Recorder::record(rcclCall_t type, ncclComm_t comm, void* handle, void* userBuffer, size_t size)
{
if (!filename.size())
{
return ncclSuccess;
}
rcclApiCall call(type);
call.comm = comm;
call.recvbuff = handle;
if (type == rrCommRegister)
{
call.sendbuff = userBuffer;
call.count = size;
}
return record(call);
}
ncclResult_t Recorder::record(rcclCall_t type, void* ptr, size_t size)
{
if (!filename.size())
{
return ncclSuccess;
}
rcclApiCall call(type);
call.recvbuff = ptr;
if (type == rrMemAlloc)
{
call.count = size;
}
return record(call);
}
void Recorder::record(int groupDepth, ncclSimInfo_t *siminfo)
{
if (!filename.size())
{
return;
}
rcclApiCall call(rrGroupSimulatedEnd);
record(call);
if (output_json && siminfo)
{
int len = snprintf(buffer, 4096, siminfo_fmt.c_str(),
siminfo->size, siminfo->magic, siminfo->version, siminfo->estimatedTime, call.timestamp);
outputFile.write(buffer, len);
} // no tid for groupCall
outputFile.flush();
}
void Recorder::record(rcclCall_t type, int size, int rank, ncclUniqueId* commId, ncclConfig_t* config, ncclComm_t comm)
{
if (!filename.size())
{
return ;
}
if (type == rrCommInitRankConfig)
{
record(type, size, rank, commId);
}
else //rrCommSplit
{
record(type, size /*color*/, rank/*key*/, commId, comm);
}
if (output_json && config)
{
int len = snprintf(buffer, 4096, config_fmt.c_str(), config->size, config->magic, config->version, config->blocking,
config->cgaClusterSize, config->minCTAs, config->maxCTAs, config->netName, config->splitShare);
outputFile.write(buffer, len);
outputFile.flush();
}
}
void Recorder::record(ncclComm_t* comms, int ndev, const int* devlist)
{
if (!filename.size())
{
return ;
}
rcclApiCall call(rrCommInitAll);
call.root = ndev;
// call.sendbuff = comms; TODO: might log this too
record(call);
if (output_json && devlist)
{
outputFile << ", devlist : [";
for (int i = 0; i < call.root - 1; i++)
outputFile << devlist[i] << ", ";
outputFile << devlist[call.root - 1] << "]";
outputFile.flush();
}
}
Recorder::~Recorder()
{
if (outputFile.is_open())
{
if (output_json) outputFile << std::endl << "}" << std::endl;
outputFile.close();
calls.clear();
}
}
static rcclCall_t getFuncType(std::string func)
{
for (int i = 0; i < sizeof(rcclCallStr) / sizeof(char*); i++)
{
if (func == std::string(rcclCallStr[i]))
{
return (rcclCall_t)i;
}
}
printf("[ERROR] Unrecognized func %s\n", func.c_str());
exit(1);
}
void parseJsonEntry(const char* entry, std::vector<rcclApiCall>& calls)
{
rcclApiCall call;
std::string str(entry);
size_t begin = str.find_first_not_of(' ');
size_t end = str.find(" : ");
rcclCall_t type = getFuncType(str.substr(begin, end-begin));
call.type = type;
switch(type) {
case rrCommRegister:
{
assert(sscanf(str.c_str() + end + 3, (ubr_fmt.substr(5) + ctxt_fmt).c_str(),
&call.comm, &call.sendbuff, &call.recvbuff, &call.count) == 4);
break;
}
case rrCommDeregister:
{
assert(sscanf(str.c_str() + end + 3, (ubDereg_fmt.substr(5) + ctxt_fmt).c_str(),
&call.comm, &call.recvbuff) == 2);
break;
}
case rrGetUniqueId:
{
assert(sscanf(str.c_str() + end + 3, (getId_fmt.substr(5) + ctxt_fmt).c_str(), &call.commId) == 1);
break;
}
case rrCommInitDev:
{
assert(sscanf(str.c_str() + end + 3, (init_fmt.substr(5) + ctxt_fmt).c_str(),
&call.comm, &call.nRanks, &call.commId, &call.globalRank, &call.root) == 5);
break;
}
case rrCommInitRank:
case rrCommInitRankConfig:
{
assert(sscanf(str.c_str() + end + 3, (rank_fmt.substr(5) + ctxt_fmt).c_str(),
&call.nRanks, &call.commId, &call.globalRank) == 3);
break;
}
case rrCommInitAll:
{
assert(sscanf(str.c_str() + end + 3, (all_fmt.substr(5) + ctxt_fmt).c_str(), &call.root) == 1);
break;
}
case rrCommFinalize:
case rrCommDestroy:
case rrCommAbort:
{
assert(sscanf(str.c_str() + end + 3, (destroy_fmt.substr(5) + ctxt_fmt).c_str(), &call.comm) == 1);
break;
}
case rrCommSplit:
{
assert(sscanf(str.c_str() + end + 3, (split_fmt.substr(5) + ctxt_fmt).c_str(),
&call.commId, &call.comm, &call.nRanks, &call.globalRank, &call.comm) == 5);
break;
}
case rrMemAlloc:
{
assert(sscanf(str.c_str() + end + 3, (alloc_fmt.substr(5) + ctxt_fmt).c_str(),
&call.recvbuff, &call.count) == 2);
break;
}
case rrMemFree:
{
assert(sscanf(str.c_str() + end + 3, (free_fmt.substr(5) + ctxt_fmt).c_str(), &call.recvbuff) == 1);
break;
}
case rrRedOpCreatePreMulSum:
{
assert(sscanf(str.c_str() + end + 3, (redop_fmt.substr(5) + ctxt_fmt).c_str(),
&call.sendbuff, &call.datatype, &call.op, &call.root, &call.comm) == 5);
break;
}
case rrRedOpDestroy:
{
assert(sscanf(str.c_str() + end + 3, (redopdestroy_fmt.substr(5) + ctxt_fmt).c_str(),
&call.op, &call.comm) == 2);
break;
}
default:
assert(sscanf(str.c_str() + end + 3, (coll_fmt.substr(5) + ctxt_fmt).c_str(),
&call.opCount, &call.sendbuff, &call.recvbuff, &call.count, &call.datatype, &call.op, &call.root,
&call.comm, &call.nRanks, &call.stream, &call.nTasks, &call.globalRank, &call.timestamp, &call.tid,
&call.hipDev, &call.graphCaptured, &call.graphID) == 17);
}
calls.push_back(call);
}
void parseBinLog()
{
// TODO: need to handle trailing data such as simInfo, devList, a2av data, etc.
}
};