Skip to content

Commit 6826099

Browse files
committed
GPU Metal: optional command-buffer timeline
With GPUCA_METAL_TIMELINE=<file>, write one line per command buffer once it completes: stream, status, CPU commit time, GPUStartTime and GPUEndTime, the kernels (with their threadgroup counts), blits, and the events it waits for and signals; and one line per host drain that had to wait. Off by default, the only cost then is a pointer test per encoded command. This is what is needed to see how the streams overlap on the GPU and where they wait for each other, without a profiler attached.
1 parent fa56a58 commit 6826099

3 files changed

Lines changed: 55 additions & 0 deletions

File tree

‎GPU/GPUTracking/Base/metal/GPUReconstructionMetal.mm‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,10 @@ uint32_t metalGPUCoreCount()
167167
mInternals->streamOrderValue[i] = 0;
168168
mInternals->streamFence[i] = [mInternals->device newFence];
169169
mInternals->streamHasWork[i] = false;
170+
mInternals->timelineKernels[i] = 0;
171+
}
172+
if (const char* timeline = getenv("GPUCA_METAL_TIMELINE")) {
173+
mInternals->timeline = fopen(timeline, "w");
170174
}
171175

172176
mInternals->mem_host = metalNewSharedBuffer(mInternals->device, mHostMemorySize);
@@ -247,6 +251,9 @@ uint32_t metalGPUCoreCount()
247251
}
248252
[mInternals->mem_host release];
249253
}
254+
if (mInternals->timeline) {
255+
fflush(mInternals->timeline); // not closed: a completion handler may still write to it
256+
}
250257

251258
[mInternals->library release];
252259
[mInternals->device release];
@@ -313,6 +320,7 @@ MetalMemRef metalResolve(const GPUReconstructionMetalInternals* internals, const
313320
id<MTLCommandBuffer> cb = mInternals->commandBuffers[realStream];
314321
id<MTLBlitCommandEncoder> blit = [cb blitCommandEncoder];
315322
metalBeginEncoder(mInternals, blit, realStream);
323+
metalTimelineAdd(mInternals, realStream, "b", nullptr, sizeBytes);
316324
[blit copyFromBuffer:s.buffer
317325
sourceOffset:s.offset
318326
toBuffer:d.buffer
@@ -346,6 +354,7 @@ MetalMemRef metalResolve(const GPUReconstructionMetalInternals* internals, const
346354
id<MTLCommandBuffer> cb = mInternals->commandBuffers[realStream];
347355
id<MTLBlitCommandEncoder> blit = [cb blitCommandEncoder];
348356
metalBeginEncoder(mInternals, blit, realStream);
357+
metalTimelineAdd(mInternals, realStream, "b", nullptr, size);
349358
[blit copyFromBuffer:s.buffer
350359
sourceOffset:s.offset
351360
toBuffer:mInternals->mem_constant
@@ -384,6 +393,7 @@ MetalMemRef metalResolve(const GPUReconstructionMetalInternals* internals, const
384393
const uint64_t value = EventValue(*ev, true);
385394
[cb encodeSignalEvent:mtlEvent value:value];
386395
metalSignalOnError(cb, mtlEvent, value);
396+
metalTimelineAdd(mInternals, stream, "s", mtlEvent, value);
387397
CommitStream(stream);
388398
}
389399

@@ -396,13 +406,27 @@ MetalMemRef metalResolve(const GPUReconstructionMetalInternals* internals, const
396406
const uint64_t value = ++mInternals->streamOrderValue[stream];
397407
[cb encodeSignalEvent:mInternals->streamOrder[stream] value:value];
398408
metalSignalOnError(cb, mInternals->streamOrder[stream], value);
409+
if (mInternals->timeline) {
410+
metalTimelineAdd(mInternals, stream, "s", mInternals->streamOrder[stream], value);
411+
FILE* f = mInternals->timeline;
412+
const std::string ops = std::move(mInternals->timelineOps[stream]);
413+
const uint32_t nKernels = mInternals->timelineKernels[stream];
414+
const uint64_t seq = mInternals->timelineBuffers++;
415+
const double commitTime = metalHostTime();
416+
[cb addCompletedHandler:^(id<MTLCommandBuffer> c) {
417+
fprintf(f, "CB %llu s%d st%d commit %.6f gpu %.6f %.6f nk %u:%s\n", (unsigned long long)seq, stream, (int)c.status, commitTime, c.GPUStartTime, c.GPUEndTime, nKernels, ops.c_str());
418+
}];
419+
mInternals->timelineOps[stream].clear();
420+
mInternals->timelineKernels[stream] = 0;
421+
}
399422
[cb commit];
400423
// Hand the committed buffer over to the stream's submission list, which owns
401424
// it until it is waited for, and open a fresh one to encode the next work in.
402425
mInternals->submittedBuffers[stream].emplace_back(cb);
403426
mInternals->streamHasWork[stream] = false;
404427
mInternals->commandBuffers[stream] = [[mInternals->commandQueues[stream] commandBuffer] retain];
405428
[mInternals->commandBuffers[stream] encodeWaitForEvent:mInternals->streamOrder[stream] value:value];
429+
metalTimelineAdd(mInternals, stream, "w", mInternals->streamOrder[stream], value);
406430
}
407431

408432
void GPUReconstructionMetal::DrainStream(int32_t stream)
@@ -421,6 +445,7 @@ MetalMemRef metalResolve(const GPUReconstructionMetalInternals* internals, const
421445
// waitUntilCompleted, which sleeps until the completion has been processed.
422446
// Only short waits are worth a busy core, so give up after a while.
423447
bool done = false;
448+
const double drainStart = mInternals->timeline && !mInternals->submittedBuffers[stream].empty() ? metalHostTime() : 0.;
424449
const auto t0 = std::chrono::steady_clock::now();
425450
while (!mInternals->submittedBuffers[stream].empty() && !(done = mInternals->streamOrder[stream].signaledValue >= mInternals->streamOrderValue[stream]) && std::chrono::steady_clock::now() - t0 < std::chrono::milliseconds(20)) {
426451
}
@@ -436,6 +461,9 @@ MetalMemRef metalResolve(const GPUReconstructionMetalInternals* internals, const
436461
[cb release];
437462
}
438463
mInternals->submittedBuffers[stream].clear();
464+
if (drainStart) {
465+
fprintf(mInternals->timeline, "DR s%d %.6f %.6f\n", stream, drainStart, metalHostTime());
466+
}
439467
// The work of a failed buffer is lost, so the results are wrong: fail like the other backends.
440468
if (error && !mInternals->exiting) {
441469
GPUChkErr(error);
@@ -469,6 +497,7 @@ MetalMemRef metalResolve(const GPUReconstructionMetalInternals* internals, const
469497
auto mtlEvent = (__bridge id<MTLSharedEvent>)(evList[ei].get<void*>());
470498
if (const uint64_t value = EventValue(evList[ei], false)) {
471499
[cb encodeWaitForEvent:mtlEvent value:value];
500+
metalTimelineAdd(mInternals, stream, "w", mtlEvent, value);
472501
}
473502
}
474503
CommitStream(stream);

‎GPU/GPUTracking/Base/metal/GPUReconstructionMetalIncludesHost.h‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ using half = _Float16;
3434
#include <memory>
3535
#include <mutex>
3636
#include <unordered_map>
37+
#include <cstdio>
38+
#include <mach/mach_time.h>
3739
#include "GPULogging.h"
3840

3941
#include "GPUReconstructionMetal.h"
@@ -68,6 +70,12 @@ struct GPUReconstructionMetalInternals {
6870
std::array<bool, constants::GPU_MAX_STREAMS> streamHasWork;
6971
// Set while the device is torn down, when a failed command buffer is no longer fatal
7072
bool exiting = false;
73+
// Opt-in command-buffer timeline, GPUCA_METAL_TIMELINE=<file>: what each open
74+
// buffer contains (kernels, blits, waits, signals), written out once it completes
75+
FILE* timeline = nullptr;
76+
std::array<std::string, constants::GPU_MAX_STREAMS> timelineOps;
77+
std::array<uint32_t, constants::GPU_MAX_STREAMS> timelineKernels;
78+
uint64_t timelineBuffers = 0;
7179

7280
std::vector<id<MTLFunction>> functions;
7381
std::vector<id<MTLComputePipelineState>> pipelines;
@@ -103,6 +111,20 @@ inline void metalSignalOnError(id<MTLCommandBuffer> cb, id<MTLSharedEvent> event
103111
}
104112
}];
105113
}
114+
// In the time base of the command buffers' GPUStartTime and GPUEndTime
115+
inline double metalHostTime()
116+
{
117+
static const double scale = [] { mach_timebase_info_data_t tb; mach_timebase_info(&tb); return 1e-9 * tb.numer / tb.denom; }();
118+
return mach_absolute_time() * scale;
119+
}
120+
inline void metalTimelineAdd(GPUReconstructionMetalInternals* internals, int32_t stream, const char* op, const void* id, uint64_t value)
121+
{
122+
if (internals->timeline) {
123+
char buf[96];
124+
snprintf(buf, sizeof(buf), " %s:%p=%llu", op, id, (unsigned long long)value);
125+
internals->timelineOps[stream] += buf;
126+
}
127+
}
106128
template <class E>
107129
inline void metalEndEncoder(GPUReconstructionMetalInternals* internals, E encoder, int32_t stream)
108130
{

‎GPU/GPUTracking/Base/metal/GPUReconstructionMetalKernels.mm‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@
5050
if (threadGroupSize > mInternals->maxThreadsPerGroup[num]) {
5151
threadGroupSize = mInternals->maxThreadsPerGroup[num];
5252
}
53+
if (mInternals->timeline) {
54+
mInternals->timelineOps[kExec.stream] += std::string(" k:") + GetKernelName<T, I>() + "/" + std::to_string(kExec.nBlocks);
55+
mInternals->timelineKernels[kExec.stream]++;
56+
}
5357
[computeEncoder dispatchThreadgroups:MTLSizeMake(kExec.nBlocks, 1, 1)
5458
threadsPerThreadgroup:MTLSizeMake(threadGroupSize, 1, 1)];
5559
metalEndEncoder(mInternals, computeEncoder, kExec.stream);

0 commit comments

Comments
 (0)