Skip to content

Commit d0fec95

Browse files
committed
GPU Metal: Metal backend initialises and exits fine
- fix lifecycle - optimise pipeline creation
1 parent 22fed33 commit d0fec95

4 files changed

Lines changed: 154 additions & 80 deletions

File tree

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ class GPUReconstructionMetal : public GPUReconstructionProcessing::KernelInterfa
5757

5858
int32_t GetMetalPrograms();
5959

60+
void CommitStream(int32_t stream);
61+
void DrainStream(int32_t stream);
62+
6063
private:
6164
int32_t AddKernels();
6265
};

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

Lines changed: 99 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99
// granted to it by virtue of its status as an Intergovernmental Organization
1010
// or submit itself to any jurisdiction.
1111

12-
#include "GPUReconstructionMetal.h"
12+
#include "GPUReconstructionMetalIncludesHost.h" // must come first
13+
1314
#include "GPUConstantMem.h"
1415
#include "GPUDefParametersLoad.inc"
15-
#include "GPUReconstructionMetalIncludesHost.h"
16+
#include "GPUReconstructionMetal.h"
1617

1718
#include <cstddef>
1819
#include <map>
@@ -100,7 +101,7 @@
100101
if (mInternals->commandQueues[i] == nil) {
101102
GPUErrorReturn("Error creating Metal command queue");
102103
}
103-
mInternals->commandBuffers[i] = [mInternals->commandQueues[i] commandBuffer];
104+
mInternals->commandBuffers[i] = [[mInternals->commandQueues[i] commandBuffer] retain];
104105
if (mInternals->commandBuffers[i] == nil) {
105106
GPUErrorReturn("Error creating Metal command buffer");
106107
}
@@ -113,9 +114,11 @@
113114

114115
mHostMemoryBase = mInternals->mem_host.contents;
115116
mHostMemorySize = mInternals->mem_host.allocatedSize;
116-
mDeviceMemoryBase = mInternals->mem_gpu.contents;
117+
// The kernels reconstruct device pointers from these bases, so they must be
118+
// the addresses the GPU sees, not the CPU-visible ones of a shared buffer.
119+
mDeviceMemoryBase = (void*)(uintptr_t)mInternals->mem_gpu.gpuAddress;
117120
mDeviceMemorySize = mInternals->mem_gpu.allocatedSize;
118-
mDeviceConstantMem = (GPUConstantMem*)mInternals->mem_constant.contents;
121+
mDeviceConstantMem = (GPUConstantMem*)(uintptr_t)mInternals->mem_constant.gpuAddress;
119122

120123
if (GetProcessingSettings().debugLevel >= 1) {
121124
GPUInfo("Memory ptrs: GPU (%ld bytes): %p - Host (%ld bytes): %p", (int64_t)mDeviceMemorySize, mDeviceMemoryBase, (int64_t)mHostMemorySize, mHostMemoryBase);
@@ -153,11 +156,17 @@
153156
[mInternals->functions[i] release];
154157
}
155158
mInternals->functions.clear();
159+
for (uint32_t i = 0; i < mInternals->pipelines.size(); i++) {
160+
[mInternals->pipelines[i] release];
161+
}
162+
mInternals->pipelines.clear();
163+
mInternals->maxThreadsPerGroup.clear();
156164
}
157165
if (mHostMemoryBase) {
158166
for (int32_t i = 0; i < mNStreams; i++) {
159-
[mInternals->commandQueues[i] release];
167+
DrainStream(i);
160168
[mInternals->commandBuffers[i] release];
169+
[mInternals->commandQueues[i] release];
161170
}
162171
[mInternals->mem_host release];
163172
}
@@ -172,6 +181,31 @@
172181
return (0);
173182
}
174183

184+
namespace
185+
{
186+
// Where a pointer lives as far as the backend is concerned. mDeviceMemoryBase is
187+
// a GPU address, so the CPU-visible address of owned memory has to be taken from
188+
// the buffer rather than from the pointer itself.
189+
struct MetalMemRef {
190+
id<MTLBuffer> buffer; // nil for memory the backend does not own
191+
size_t offset;
192+
char* cpu;
193+
};
194+
195+
MetalMemRef metalResolve(const GPUReconstructionMetalInternals* internals, const void* p, const void* hostBase, size_t hostSize, const void* devBase, size_t devSize)
196+
{
197+
if (p >= hostBase && p < ((const char*)hostBase + hostSize)) {
198+
const size_t o = (const char*)p - (const char*)hostBase;
199+
return {internals->mem_host, o, (char*)internals->mem_host.contents + o};
200+
}
201+
if (p >= devBase && p < ((const char*)devBase + devSize)) {
202+
const size_t o = (const char*)p - (const char*)devBase;
203+
return {internals->mem_gpu, o, (char*)internals->mem_gpu.contents + o};
204+
}
205+
return {nil, 0, (char*)p};
206+
}
207+
} // namespace
208+
175209
size_t GPUReconstructionMetal::GPUMemCpy(void* dst, const void* src, size_t sizeBytes, int32_t stream, int32_t toGPU, deviceEvent* ev, deviceEvent* evList, int32_t nEvents)
176210
{
177211
if (evList == nullptr) {
@@ -186,42 +220,27 @@
186220
}
187221

188222
auto realStream = stream == -1 ? 0 : stream;
189-
id<MTLCommandBuffer> cb = mInternals->commandBuffers[realStream];
190-
id<MTLBlitCommandEncoder> blit = [cb blitCommandEncoder];
191-
id<MTLBuffer> sourceBuffer = nil;
192-
id<MTLBuffer> destBuffer = nil;
193-
ptrdiff_t sourceOffset = 0;
194-
ptrdiff_t destOffset = 0;
195-
196-
if (src >= mHostMemoryBase && src < ((char*)mHostMemoryBase + mHostMemorySize)) {
197-
sourceBuffer = mInternals->mem_host;
198-
sourceOffset = (char*)src - (char*)mHostMemoryBase;
199-
} else if (src >= mDeviceMemoryBase && src < ((char*)mDeviceMemoryBase + mDeviceMemorySize)) {
200-
sourceBuffer = mInternals->mem_gpu;
201-
sourceOffset = (char*)src - (char*)mDeviceMemoryBase;
223+
const MetalMemRef s = metalResolve(mInternals, src, mHostMemoryBase, mHostMemorySize, mDeviceMemoryBase, mDeviceMemorySize);
224+
const MetalMemRef d = metalResolve(mInternals, dst, mHostMemoryBase, mHostMemorySize, mDeviceMemoryBase, mDeviceMemorySize);
225+
226+
if (s.buffer == nil || d.buffer == nil) {
227+
// A blit moves data between MTLBuffers only, and the calibration objects
228+
// live in ordinary memory. Both pools are shared storage on a unified
229+
// architecture, so the copy can be done on the host once the stream is idle.
230+
DrainStream(realStream);
231+
memcpy(d.cpu, s.cpu, sizeBytes);
202232
} else {
203-
GPUErrorReturn("Unknown buffer at %p", src);
233+
id<MTLCommandBuffer> cb = mInternals->commandBuffers[realStream];
234+
id<MTLBlitCommandEncoder> blit = [cb blitCommandEncoder];
235+
[blit copyFromBuffer:s.buffer
236+
sourceOffset:s.offset
237+
toBuffer:d.buffer
238+
destinationOffset:d.offset
239+
size:sizeBytes];
240+
[blit endEncoding];
241+
CommitStream(realStream);
204242
}
205243

206-
if (dst >= mHostMemoryBase && dst < ((char*)mHostMemoryBase + mHostMemorySize)) {
207-
destBuffer = mInternals->mem_host;
208-
destOffset = (char*)dst - (char*)mHostMemoryBase;
209-
} else if (dst >= mDeviceMemoryBase && dst < ((char*)mDeviceMemoryBase + mDeviceMemorySize)) {
210-
destBuffer = mInternals->mem_gpu;
211-
destOffset = (char*)dst - (char*)mDeviceMemoryBase;
212-
} else {
213-
GPUErrorReturn("Unknown buffer at %p", dst);
214-
}
215-
216-
[blit copyFromBuffer:sourceBuffer
217-
sourceOffset:sourceOffset
218-
toBuffer:destBuffer
219-
destinationOffset:destOffset
220-
size:sizeBytes];
221-
222-
[blit endEncoding];
223-
[cb commit];
224-
225244
if (GetProcessingSettings().serializeGPU & 2) {
226245
GPUDebug(("GPUMemCpy " + std::to_string(toGPU)).c_str(), stream, true);
227246
}
@@ -235,27 +254,22 @@
235254
}
236255

237256
auto realStream = stream == -1 ? 0 : stream;
238-
id<MTLCommandBuffer> cb = mInternals->commandBuffers[realStream];
239-
id<MTLBlitCommandEncoder> blit = [cb blitCommandEncoder];
240-
id<MTLBuffer> sourceBuffer = nil;
241-
ptrdiff_t sourceOffset = 0;
242-
if (src >= mHostMemoryBase && src < ((char*)mHostMemoryBase + mHostMemorySize)) {
243-
sourceBuffer = mInternals->mem_host;
244-
sourceOffset = (char*)src - (char*)mHostMemoryBase;
245-
} else if (src >= mDeviceMemoryBase && src < ((char*)mDeviceMemoryBase + mDeviceMemorySize)) {
246-
sourceBuffer = mInternals->mem_gpu;
247-
sourceOffset = (char*)src - (char*)mDeviceMemoryBase;
257+
const MetalMemRef s = metalResolve(mInternals, src, mHostMemoryBase, mHostMemorySize, mDeviceMemoryBase, mDeviceMemorySize);
258+
259+
if (s.buffer == nil) {
260+
DrainStream(realStream);
261+
memcpy((char*)mInternals->mem_constant.contents + offset, s.cpu, size);
248262
} else {
249-
GPUErrorReturn("Unknown buffer at %p", src);
263+
id<MTLCommandBuffer> cb = mInternals->commandBuffers[realStream];
264+
id<MTLBlitCommandEncoder> blit = [cb blitCommandEncoder];
265+
[blit copyFromBuffer:s.buffer
266+
sourceOffset:s.offset
267+
toBuffer:mInternals->mem_constant
268+
destinationOffset:offset
269+
size:size];
270+
[blit endEncoding];
271+
CommitStream(realStream);
250272
}
251-
[blit copyFromBuffer:sourceBuffer
252-
sourceOffset:sourceOffset
253-
toBuffer:mInternals->mem_constant
254-
destinationOffset:offset
255-
size:size];
256-
257-
[blit endEncoding];
258-
[cb commit];
259273

260274
if (GetProcessingSettings().serializeGPU & 2) {
261275
GPUDebug("WriteToConstantMemory", stream, true);
@@ -278,26 +292,46 @@
278292
// the lifetime of the events outside here.
279293
auto mtlEvent = (__bridge id<MTLSharedEvent>)(ev->get<void*>());
280294
[cb encodeSignalEvent:mtlEvent value:1];
295+
CommitStream(stream);
296+
}
297+
298+
void GPUReconstructionMetal::CommitStream(int32_t stream)
299+
{
300+
id<MTLCommandBuffer> cb = mInternals->commandBuffers[stream];
281301
[cb commit];
302+
// Hand the committed buffer over to the stream's submission list, which owns
303+
// it until it is waited for, and open a fresh one to encode the next work in.
304+
mInternals->submittedBuffers[stream].emplace_back(cb);
305+
mInternals->commandBuffers[stream] = [[mInternals->commandQueues[stream] commandBuffer] retain];
306+
}
307+
308+
void GPUReconstructionMetal::DrainStream(int32_t stream)
309+
{
310+
// Waiting on the buffer that is currently open would block forever: it is not
311+
// committed, so it never completes. Only what was submitted can be waited for.
312+
for (id<MTLCommandBuffer> cb : mInternals->submittedBuffers[stream]) {
313+
[cb waitUntilCompleted];
314+
[cb release];
315+
}
316+
mInternals->submittedBuffers[stream].clear();
282317
}
283318

284319
void GPUReconstructionMetal::SynchronizeGPU()
285320
{
286321
for (int32_t i = 0; i < mNStreams; i++) {
287-
[mInternals->commandBuffers[i] waitUntilCompleted];
322+
DrainStream(i);
288323
}
289324
}
290325

291326
void GPUReconstructionMetal::SynchronizeStream(int32_t stream)
292327
{
293-
[mInternals->commandBuffers[stream] waitUntilCompleted];
328+
DrainStream(stream);
294329
}
295330

296331
void GPUReconstructionMetal::SynchronizeEvents(deviceEvent* evList, int32_t nEvents)
297332
{
298333
for (int32_t si = 0; si < mNStreams; si++) {
299-
id<MTLCommandBuffer> cb = mInternals->commandBuffers[si];
300-
[cb waitUntilCompleted];
334+
DrainStream(si);
301335
}
302336
}
303337

@@ -309,8 +343,8 @@
309343
auto mtlEvent = (__bridge id<MTLSharedEvent>)(evList[ei].get<void*>());
310344
[cb encodeWaitForEvent:mtlEvent value:1];
311345
}
312-
[cb commit];
313-
[cb waitUntilCompleted];
346+
CommitStream(stream);
347+
DrainStream(stream);
314348
}
315349

316350
bool GPUReconstructionMetal::IsEventDone(deviceEvent* evList, int32_t nEvents)
@@ -331,7 +365,7 @@
331365
return (0);
332366
}
333367
for (int32_t si = 0; si < mNStreams; si++) {
334-
[mInternals->commandBuffers[si] waitUntilCompleted];
368+
DrainStream(si);
335369
}
336370
if (GetProcessingSettings().debugLevel >= 3) {
337371
GPUInfo("GPU Sync Done");

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,19 @@
1212
#ifndef GPURECONSTRUCTIONOMETALINCLUDESHOST_H
1313
#define GPURECONSTRUCTIONOMETALINCLUDESHOST_H
1414

15+
#ifdef GPUCOMMONDEF_H
16+
#error "this header establishes GPUCA_GPUTYPE_METAL and __METAL_HOST__, so it must be included before any other GPU header"
17+
#endif
18+
1519
#define GPUCA_GPUTYPE_METAL
1620

1721
#import <Metal/Metal.h>
1822
#ifndef __METAL_VERSION__
1923
// __METAL_VERSION__ is only defined in device code.
2024
#define __METAL_HOST__
25+
// The parameter table picks MSL's `half` for the device layouts this translation
26+
// unit has to agree with; _Float16 is the same IEEE binary16 type.
27+
using half = _Float16;
2128
#endif
2229

2330
#import <Foundation/Foundation.h>
@@ -45,10 +52,15 @@ struct GPUReconstructionMetalInternals {
4552
id<MTLDevice> device;
4653

4754
std::array<id<MTLCommandQueue>, constants::GPU_MAX_STREAMS> commandQueues;
55+
// The buffer currently open for encoding, plus the ones already committed and
56+
// not yet waited for. A Metal command buffer takes one commit and cannot be
57+
// encoded into afterwards, so the two cannot be the same object.
4858
std::array<id<MTLCommandBuffer>, constants::GPU_MAX_STREAMS> commandBuffers;
59+
std::array<std::vector<id<MTLCommandBuffer>>, constants::GPU_MAX_STREAMS> submittedBuffers;
4960

5061
std::vector<id<MTLFunction>> functions;
5162
std::vector<id<MTLComputePipelineState>> pipelines;
63+
std::vector<uint32_t> maxThreadsPerGroup;
5264

5365
id<MTLBuffer> mem_gpu;
5466
id<MTLBuffer> mem_constant;

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

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@
99
// granted to it by virtue of its status as an Intergovernmental Organization
1010
// or submit itself to any jurisdiction.
1111

12+
#include "GPUReconstructionMetalIncludesHost.h" // must come first
13+
1214
#include <Foundation/Foundation.h>
1315
#include "GPUReconstructionKernelIncludes.h"
14-
#include "GPUReconstructionMetalIncludesHost.h"
1516

1617
#include "GPUReconstructionMetalKernelsSpecialize.inc"
1718
#include "GPUReconstructionProcessingKernels.inc"
@@ -21,30 +22,35 @@
2122
template <class T, int32_t I, typename... Args>
2223
inline void GPUReconstructionMetal::runKernelBackend(const krnlSetupTime& _xyz, const Args&... args)
2324
{
24-
id<MTLFunction> function = mInternals->functions[GetKernelNum<T, I>()];
25+
const uint32_t num = GetKernelNum<T, I>();
2526
auto& kExec = _xyz.x;
2627
auto& runRange = _xyz.y;
2728

28-
NSError* error = nil;
29-
auto pso = [mInternals->device newComputePipelineStateWithFunction:function error:&error];
29+
id<MTLComputePipelineState> pso = mInternals->pipelines[num];
3030
id<MTLComputeCommandEncoder> computeEncoder = [mInternals->commandBuffers[kExec.stream] computeCommandEncoder];
3131

32-
// Map buffers and states
32+
// Slots 0 to 2 are what GPUCA_KRNL_BUFFERS and GPUCA_KRNL_SECTOR_ARG declare
33+
// in the generated signature; the kernel's own arguments follow from slot 3.
3334
[computeEncoder setComputePipelineState:pso];
3435
[computeEncoder setBuffer:mInternals->mem_gpu offset:0 atIndex:0];
3536
[computeEncoder setBuffer:mInternals->mem_constant offset:0 atIndex:1];
36-
[computeEncoder setBuffer:mInternals->mem_host offset:0 atIndex:2];
37-
38-
MTLSize gridSize = MTLSizeMake(runRange.index, 1, 1);
37+
const int32_t iSector = runRange.index;
38+
[computeEncoder setBytes:&iSector length:sizeof(iSector) atIndex:2];
39+
NSUInteger argIndex = 3;
40+
((void)[&](const auto& arg) { [computeEncoder setBytes:&arg length:sizeof(arg) atIndex:argIndex++]; }(args), ...);
3941

40-
NSUInteger threadGroupSize = pso.maxTotalThreadsPerThreadgroup;
41-
if (threadGroupSize > runRange.index) {
42-
threadGroupSize = runRange.index;
42+
// runKernelInterface has already resolved the grid, so nBlocks and nThreads
43+
// carry the tuned launch bounds and map onto Metal's threadgroups directly.
44+
NSUInteger threadGroupSize = kExec.nThreads;
45+
if (threadGroupSize > mInternals->maxThreadsPerGroup[num]) {
46+
threadGroupSize = mInternals->maxThreadsPerGroup[num];
4347
}
44-
45-
MTLSize threadgroupSize = MTLSizeMake(threadGroupSize, 1, 1);
46-
[computeEncoder dispatchThreads:gridSize
47-
threadsPerThreadgroup:threadgroupSize];
48+
[computeEncoder dispatchThreadgroups:MTLSizeMake(kExec.nBlocks, 1, 1)
49+
threadsPerThreadgroup:MTLSizeMake(threadGroupSize, 1, 1)];
50+
// Without ending the encoder the next one on this buffer is rejected, and
51+
// without committing the kernel never reaches the device.
52+
[computeEncoder endEncoding];
53+
CommitStream(kExec.stream);
4854
}
4955

5056
template <class T, int32_t I>
@@ -58,7 +64,26 @@
5864
return 1;
5965
}
6066

67+
NSError* error = nil;
68+
id<MTLComputePipelineState> pso = [mInternals->device newComputePipelineStateWithFunction:krnl error:&error];
69+
if (pso == nil) {
70+
NSLog(@"%@", error);
71+
GPUError("Error creating Metal pipeline state: %s", [kname cStringUsingEncoding:NSUTF8StringEncoding]);
72+
return 1;
73+
}
74+
75+
// The kernel sizes its threadgroup memory from the tuning table at compile
76+
// time, so a launch must not exceed what it was built for either way.
77+
const uint32_t tunedThreads = this->template getKernelProperties<T, I>().nThreads;
78+
if (tunedThreads > pso.maxTotalThreadsPerThreadgroup) {
79+
// Fewer threads than the kernel was built for: safe for the shared arrays,
80+
// but the scan and compaction kernels assume every slot has an owner.
81+
GPUError("Metal kernel %s is tuned for %u threads but this device allows only %lu per threadgroup", GetKernelName<T, I>(), tunedThreads, (unsigned long)pso.maxTotalThreadsPerThreadgroup);
82+
}
83+
6184
mInternals->functions.emplace_back(krnl);
85+
mInternals->pipelines.emplace_back(pso);
86+
mInternals->maxThreadsPerGroup.emplace_back(pso.maxTotalThreadsPerThreadgroup);
6287
return 0;
6388
}
6489

0 commit comments

Comments
 (0)