Skip to content

Commit 1eb67a0

Browse files
committed
GPU Metal: optionally limit the GPU work in flight
At 20 kHz occupancy the macOS GPU watchdog killed command buffers, and reset the desktop once, while all 8 streams ran multi-kernel buffers of up to ~1 s at the same time. Host-serialised execution (serializeGPU 3) was the only mode that survived. Add two runtime limits, off by default: - GPUCA_METAL_MAX_KERNELS_PER_BUFFER=N commits a stream's command buffer after N kernels, so that long chains (sector tracking, SectorRefit, TrackFit) become short buffers. Buffers of a stream stay ordered by its order event, the events of the other streams are untouched. - GPUCA_METAL_MAX_STREAMS_IN_FLIGHT=M makes the host wait, before it commits a buffer on an idle stream, until fewer than M streams have committed buffers that are not complete. This cannot deadlock: a buffer only waits for events signalled by buffers committed before it, since RecordMarker commits the signal at once, so the work in flight completes without the host. pbpb30, M1 Max, 6 rounds of 10 events (timeline: longest buffer, streams running at once): default unchanged (931 ms/ev, -3 ms over two run orders); N=1 M=1 1416 ms/ev, 87 ms, 1 stream (serializeGPU 3: 1413 ms/ev); N=1 M=2 1100 ms/ev, 87 ms, 2; N=2 M=4 941 ms/ev, 123 ms, 4. The longest buffers left hold the TrackFit kernel (~85 ms alone). Metal dumps byte-identical on pbpb30 with and without the limits, sim5pp identical to the CPU.
1 parent 0cedf04 commit 1eb67a0

3 files changed

Lines changed: 42 additions & 0 deletions

File tree

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ uint32_t metalGPUCoreCount()
172172
if (const char* timeline = getenv("GPUCA_METAL_TIMELINE")) {
173173
mInternals->timeline = fopen(timeline, "w");
174174
}
175+
mInternals->maxKernelsPerBuffer = getenv("GPUCA_METAL_MAX_KERNELS_PER_BUFFER") ? atoi(getenv("GPUCA_METAL_MAX_KERNELS_PER_BUFFER")) : 0;
176+
mInternals->maxStreamsInFlight = getenv("GPUCA_METAL_MAX_STREAMS_IN_FLIGHT") ? atoi(getenv("GPUCA_METAL_MAX_STREAMS_IN_FLIGHT")) : 0;
175177

176178
mInternals->mem_host = metalNewSharedBuffer(mInternals->device, mHostMemorySize);
177179
if (mInternals->mem_host == nil) {
@@ -251,6 +253,9 @@ uint32_t metalGPUCoreCount()
251253
}
252254
[mInternals->mem_host release];
253255
}
256+
// The completion handlers of the limit still use the internals
257+
std::unique_lock<std::mutex> lock(mInternals->inFlightMutex);
258+
mInternals->inFlightDone.wait(lock, [this] { return mInternals->streamsInFlight == 0; });
254259
if (mInternals->timeline) {
255260
fflush(mInternals->timeline); // not closed: a completion handler may still write to it
256261
}
@@ -406,6 +411,9 @@ MetalMemRef metalResolve(const GPUReconstructionMetalInternals* internals, const
406411
const uint64_t value = ++mInternals->streamOrderValue[stream];
407412
[cb encodeSignalEvent:mInternals->streamOrder[stream] value:value];
408413
metalSignalOnError(cb, mInternals->streamOrder[stream], value);
414+
if (mInternals->maxStreamsInFlight) {
415+
metalLimitStreamsInFlight(mInternals, cb, stream);
416+
}
409417
if (mInternals->timeline) {
410418
metalTimelineAdd(mInternals, stream, "s", mInternals->streamOrder[stream], value);
411419
FILE* f = mInternals->timeline;
@@ -424,6 +432,7 @@ MetalMemRef metalResolve(const GPUReconstructionMetalInternals* internals, const
424432
// it until it is waited for, and open a fresh one to encode the next work in.
425433
mInternals->submittedBuffers[stream].emplace_back(cb);
426434
mInternals->streamHasWork[stream] = false;
435+
mInternals->streamKernels[stream] = 0;
427436
mInternals->commandBuffers[stream] = [[mInternals->commandQueues[stream] commandBuffer] retain];
428437
[mInternals->commandBuffers[stream] encodeWaitForEvent:mInternals->streamOrder[stream] value:value];
429438
metalTimelineAdd(mInternals, stream, "w", mInternals->streamOrder[stream], value);

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ using half = _Float16;
3333
#include <string>
3434
#include <memory>
3535
#include <mutex>
36+
#include <condition_variable>
3637
#include <unordered_map>
3738
#include <cstdio>
3839
#include <mach/mach_time.h>
@@ -76,6 +77,17 @@ struct GPUReconstructionMetalInternals {
7677
std::array<std::string, constants::GPU_MAX_STREAMS> timelineOps;
7778
std::array<uint32_t, constants::GPU_MAX_STREAMS> timelineKernels;
7879
uint64_t timelineBuffers = 0;
80+
// Opt-in limits on the GPU work in flight, 0 = none, so that no stretch of GPU
81+
// work runs long enough for the macOS GPU watchdog: GPUCA_METAL_MAX_KERNELS_PER_BUFFER
82+
// commits a stream's buffer after that many kernels, GPUCA_METAL_MAX_STREAMS_IN_FLIGHT
83+
// is how many streams may have committed buffers that are not complete yet.
84+
uint32_t maxKernelsPerBuffer = 0;
85+
uint32_t maxStreamsInFlight = 0;
86+
std::array<uint32_t, constants::GPU_MAX_STREAMS> streamKernels{};
87+
std::array<uint32_t, constants::GPU_MAX_STREAMS> streamBuffersInFlight{};
88+
uint32_t streamsInFlight = 0;
89+
std::mutex inFlightMutex;
90+
std::condition_variable inFlightDone;
7991

8092
std::vector<id<MTLFunction>> functions;
8193
std::vector<id<MTLComputePipelineState>> pipelines;
@@ -111,6 +123,25 @@ inline void metalSignalOnError(id<MTLCommandBuffer> cb, id<MTLSharedEvent> event
111123
}
112124
}];
113125
}
126+
// Before a buffer of an idle stream is committed, wait until fewer than the
127+
// limit of streams have work in flight. This cannot deadlock: a buffer only waits
128+
// for events that buffers committed before it signal (RecordMarker commits the
129+
// signal at once), so the work in flight completes without the host's help.
130+
inline void metalLimitStreamsInFlight(GPUReconstructionMetalInternals* internals, id<MTLCommandBuffer> cb, int32_t stream)
131+
{
132+
std::unique_lock<std::mutex> lock(internals->inFlightMutex);
133+
if (internals->streamBuffersInFlight[stream]++ == 0) {
134+
internals->inFlightDone.wait(lock, [internals] { return internals->streamsInFlight < internals->maxStreamsInFlight; });
135+
internals->streamsInFlight++;
136+
}
137+
[cb addCompletedHandler:^(id<MTLCommandBuffer>) {
138+
std::lock_guard<std::mutex> l(internals->inFlightMutex);
139+
if (--internals->streamBuffersInFlight[stream] == 0) {
140+
internals->streamsInFlight--;
141+
internals->inFlightDone.notify_all();
142+
}
143+
}];
144+
}
114145
// In the time base of the command buffers' GPUStartTime and GPUEndTime
115146
inline double metalHostTime()
116147
{

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@
6565
id<MTLCommandBuffer> cb = [mInternals->commandBuffers[kExec.stream] retain];
6666
if (kEvents.ev) {
6767
RecordMarker(kEvents.ev, kExec.stream);
68+
} else if (mInternals->maxKernelsPerBuffer && ++mInternals->streamKernels[kExec.stream] >= mInternals->maxKernelsPerBuffer) {
69+
CommitStream(kExec.stream);
6870
}
6971
if (timed) {
7072
if (mInternals->commandBuffers[kExec.stream] == cb) {

0 commit comments

Comments
 (0)