From 6836a22c2efe2c2bccb2c1d122c48e64daaff0d2 Mon Sep 17 00:00:00 2001 From: Giulio Eulisse <10544+ktf@users.noreply.github.com> Date: Fri, 6 Nov 2026 14:22:31 +0100 Subject: [PATCH 1/3] GPU Metal: hand the GPU the open work of all streams before waiting A stream's command buffer is committed only where something waits for it, so while the host drained one stream the GPU did not see what the other streams had encoded. In the merging across sector borders, which sorts on the host between kernels on alternating streams, each drain let the GPU run only the stream waited for, then idle while the host sorted: about 70 ms per event of 30-collision Pb-Pb. Commit the open buffers of all streams before waiting. In a Metal System Trace this also lets the streams overlap on the GPU, which they did not before. On an M1 Max the 30-collision Pb-Pb time frame takes 1204 ms per event instead of 1364 (4 interleaved rounds of 10 runs, load average 3 to 4), and the clusters are byte-identical to the CPU's. --- GPU/GPUTracking/Base/metal/GPUReconstructionMetal.mm | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/GPU/GPUTracking/Base/metal/GPUReconstructionMetal.mm b/GPU/GPUTracking/Base/metal/GPUReconstructionMetal.mm index 26d7281439c92..6ada7feacca3d 100644 --- a/GPU/GPUTracking/Base/metal/GPUReconstructionMetal.mm +++ b/GPU/GPUTracking/Base/metal/GPUReconstructionMetal.mm @@ -376,8 +376,12 @@ MetalMemRef metalResolve(const GPUReconstructionMetalInternals* internals, const void GPUReconstructionMetal::DrainStream(int32_t stream) { - if (mInternals->streamHasWork[stream]) { - CommitStream(stream); + // Hand the GPU what the other streams have encoded too, so it has work + // while the host waits and then works on what it waited for. + for (int32_t i = 0; i < mNStreams; i++) { + if (mInternals->streamHasWork[i]) { + CommitStream(i); + } } // Waiting on the buffer that is currently open would block forever: it is not // committed, so it never completes. Only what was submitted can be waited for. From 56ed42e0260980e96ce31cc0847e446d22956221 Mon Sep 17 00:00:00 2001 From: Giulio Eulisse <10544+ktf@users.noreply.github.com> Date: Fri, 6 Nov 2026 10:51:42 +0100 Subject: [PATCH 2/3] GPU Metal: watch for the end of a stream's work before sleeping on it DrainStream slept in waitUntilCompleted, which returns once the completion of the buffer has been processed, well after the GPU is done. With about 800 such waits per event of 30-collision Pb-Pb, the GPU sat idle for 58 to 71 ms per event while the host was still waiting in it, as os_signpost intervals around the wait show against the GPU state in a Metal System Trace. The last submitted buffer of a stream signals the stream's order event when all the work before it is done, so watch that event for up to 20 ms first, and sleep only for longer waits. On an M1 Max the 30-collision Pb-Pb time frame takes 1184 ms per event instead of 1210 (4 interleaved rounds of 10 runs, load average 4 to 5; waiting at most 5 ms gave 1192, at most 1 ms no gain), and the clusters are byte-identical to the CPU's. --- .../Base/metal/GPUReconstructionMetal.mm | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/GPU/GPUTracking/Base/metal/GPUReconstructionMetal.mm b/GPU/GPUTracking/Base/metal/GPUReconstructionMetal.mm index 6ada7feacca3d..338a421df406b 100644 --- a/GPU/GPUTracking/Base/metal/GPUReconstructionMetal.mm +++ b/GPU/GPUTracking/Base/metal/GPUReconstructionMetal.mm @@ -15,6 +15,7 @@ #include "GPUDefParametersLoad.inc" #include "GPUReconstructionMetal.h" +#include #include #include @@ -385,8 +386,18 @@ MetalMemRef metalResolve(const GPUReconstructionMetalInternals* internals, const } // Waiting on the buffer that is currently open would block forever: it is not // committed, so it never completes. Only what was submitted can be waited for. + // The last submitted buffer ends by signalling the stream's order event, once + // all the work before it is done. Watching the event returns sooner than + // waitUntilCompleted, which sleeps until the completion has been processed. + // Only short waits are worth a busy core, so give up after a while. + bool done = false; + const auto t0 = std::chrono::steady_clock::now(); + while (!mInternals->submittedBuffers[stream].empty() && !(done = mInternals->streamOrder[stream].signaledValue >= mInternals->streamOrderValue[stream]) && std::chrono::steady_clock::now() - t0 < std::chrono::milliseconds(20)) { + } for (id cb : mInternals->submittedBuffers[stream]) { - [cb waitUntilCompleted]; + if (!done) { + [cb waitUntilCompleted]; + } if (cb.status == MTLCommandBufferStatusError) { GPUError("Metal command buffer on stream %d failed: %s", stream, [[cb.error description] UTF8String]); } From 12bdfeb7077721a88c7967ff15483711a3a9f392 Mon Sep 17 00:00:00 2001 From: Giulio Eulisse <10544+ktf@users.noreply.github.com> Date: Fri, 6 Nov 2026 11:58:56 +0100 Subject: [PATCH 3/3] GPU: one thread per threadgroup for the looper following on Metal GPUTPCGMMergerFollowLoopers follows each looper in one thread through a long serial loop of propagation steps and cluster searches. There are few loopers, about 206 per event of 30-collision Pb-Pb, so with 256 threads per group they all landed in one threadgroup, where the lanes of each SIMD group take different paths and run one after the other: 33 to 41 ms per event in a Metal System Trace, against 0.6 ms on the CPU. Without the cluster searches the kernel took 0.6 ms, and 32 or 64 threads per group did not help. With one thread per group every looper runs in its own SIMD group; 16 groups per GPU core leave room for twice as many loopers as here. On an M1 Max the kernel took 3.6 to 8.5 ms per event in a trace with 8 groups per core, and the 30-collision Pb-Pb time frame takes 1058 ms per event instead of 1088 with 8 or 16 groups per core alike (4 interleaved rounds of 10 runs, load average 4 to 7). The clusters are byte-identical to the CPU's. --- GPU/GPUTracking/Definitions/Parameters/GPUParameters.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GPU/GPUTracking/Definitions/Parameters/GPUParameters.csv b/GPU/GPUTracking/Definitions/Parameters/GPUParameters.csv index 060ae99bffc37..fba91d5366b52 100644 --- a/GPU/GPUTracking/Definitions/Parameters/GPUParameters.csv +++ b/GPU/GPUTracking/Definitions/Parameters/GPUParameters.csv @@ -28,7 +28,7 @@ GPUTPCCFDecodeZSDenseLink,"""GPUCA_WARP_SIZE""",,"[""GPUCA_WARP_SIZE"", 4]","["" GPUTPCCFGather,"[1024, 1]",,"[1024, 5]","[1024, 1]",,,,,,"[160, 11]","[1024, 1]",736,896,,"[928, 1]",,"[1024, 1]","[320, 2]", COMPRESSION_GATHER,1024,,1024,1024,,,,,,1024,1024,,1024,,,,,, GPUTPCGMMergerTrackFit,256,,"[192, 2]","[64, 7]",,,,,,"[32, 16]","[32, 8]","[32, 14]","[160, 2]",,"[32, 24]",,"[64, 8]","[64, 6]","[64, 4]" -GPUTPCGMMergerFollowLoopers,256,,"[256, 5]","[256, 4]",,,,,,"[256, 4]","[128, 4]","[1024, 1]",640,,"[128, 16]",,"[224, 3]","[256, 7]", +GPUTPCGMMergerFollowLoopers,256,,"[256, 5]","[256, 4]",,,,,,"[256, 4]","[128, 4]","[1024, 1]",640,,"[128, 16]",,"[224, 3]","[256, 7]","[1, 16]" GPUTPCGMMergerSectorRefit,256,,"[64, 4]","[256, 2]",,,,,,"[32, 8]","[64, 5]","[32, 7]","[32, 7]",,"[32, 20]",,"[32, 10]","[64, 4]", GPUTPCGMMergerUnpackResetIds,256,,256,256,,,,,,256,256,256,256,,256,,256,256, GPUTPCGMMergerUnpackGlobal,256,,256,256,,,,,,256,256,256,256,,256,,256,256,