Repository navigation
GPU: make the kernel entry-point signature work on Metal - #15895
Conversation
|
@davidrohr any objections? in particular on how the GPUCA_KRNL_SECTOR_ARG and GPUCA_KRNL_GRID_ARGS were implemented? |
| #endif | ||
| #define GPUCA_KRNLGPU_DEF(x_class, x_attributes, x_arguments, ...) \ | ||
| GPUg() void GPUCA_ATTRRES(GPUCA_M_STRIP(x_attributes)) GPUCA_M_CAT(krnl_, GPUCA_M_KRNL_NAME(x_class))(GPUCA_CONSMEM_PTR int32_t _iSector_internal GPUCA_M_STRIP(x_arguments)) | ||
| GPUg() void GPUCA_ATTRRES(GPUCA_M_STRIP(x_attributes)) GPUCA_M_CAT(krnl_, GPUCA_M_KRNL_NAME(x_class))(GPUCA_CONSMEM_PTR GPUCA_KRNL_SECTOR_ARG GPUCA_M_STRIP(x_arguments) GPUCA_KRNL_GRID_ARGS) |
There was a problem hiding this comment.
Hm, but this means that you pass in local and global id and size as argument to the kernel function.
However, in OpenCL / CUDA / HIP, these varaibles are available everywhere, without being passed in.
I.e., they are also available in subfunctions. And I don't want to pass them in explicitly to each place where they are used. Is this somehow possible with metal?
There was a problem hiding this comment.
As far as I understand, no, and it's a limitation / design choice of the internal representation which does not expose any getter for the thread-related indices. They need to be passed as specially marked arguments. I guess the idea is that signatures are more "functional" such a way and there is no hidden state in the functions. This is no different from what happens on the CPU where you expect that 4 of the 6 OpenCL helpers are provided as parameters / available in scope. This does the same for the other two.
I've reordered the series so this migration comes first, ahead of any Metal change — on its own it is backend-neutral: it rewrites 105 uses of the six helpers across 21 files, and only four functions gain an index parameter (sortInBlock, buildCluster, findMinimaAndPeaks, isPeak). That should let you evaluate the impact of the whole change, and then we can decide.
As a side benefit, the index arithmetic no longer assumes one thread per block, so it is correct on the CPU for any nThreads. Parallelism over blocks is already there; this would make it possible to also use the thread dimension within a block on the host, if desired / supported by TBB.
| // grid dimensions the same way, so the backend gets to shape both ends of the | ||
| // parameter list. | ||
| #ifndef GPUCA_KRNL_SECTOR_ARG | ||
| #define GPUCA_KRNL_SECTOR_ARG int32_t _iSector_internal |
There was a problem hiding this comment.
I don't understand why you need a special treatment for the sector variable in metal?
The sector variable is a normal variable, which is passed in like any other parameter to function calls.
There was a problem hiding this comment.
It needs to be bound to a buffer. There is some buffer counting logic which was in a subsequent commit and now sits together with this one.
MSL has no ambient work-item builtins: the thread and threadgroup positions exist only as attributes on the kernel entry point, so get_local_id() and its siblings cannot read them from a device function the way CUDA's threadIdx or OpenCL's get_local_id() can. Almost every call site is already inside a function that receives nBlocks, nThreads, iBlock and iThread, which is how the CPU backend has always worked: four of the six helpers expand to the bare iBlock and nBlocks there, so they only compile where those are in scope. Four functions have no index in scope at all and get them passed in: sortInBlock, GPUTPCCFClusterizer::buildCluster, GPUTPCCFNoiseSuppression::findMinimaAndPeaks and GPUTPCCFPeakFinder::isPeak. GPUCA_THREAD_INFO_DECL and GPUCA_THREAD_INFO_PROVIDE gate that so only Metal pays for it: both expand to nothing on every other backend, and the helpers keep reading get_local_id(0) rather than open-coding the index arithmetic.
MSL has no by-value kernel parameters: every one must carry an attribute naming a buffer or a builtin. It also has no ambient work-item builtins, so the grid dimensions arrive as attributes on the entry point too. GPUCA_KRNLGPU_DEF therefore gets two hooks, GPUCA_KRNL_SECTOR_ARG and GPUCA_KRNL_GRID_ARGS, which the Metal source fills in with a buffer and the four grid attributes. Both default to what the signature had, so CUDA, HIP and OpenCL generate exactly the same entry point as before. The attributes are named after the nBlocks, nThreads, iBlock and iThread that Thread() already takes, so the get_*() helpers resolve at the entry point and in everything it calls. Metal still needs its own definitions of them because the host ones assume one thread per block: get_local_id() is 0 and get_local_size() is 1 there. The generated arguments need an attribute too, each with a distinct buffer index, and the preprocessor cannot supply one: the kernel list splices arguments as a flat comma-separated list, and __COUNTER__ is monotonic across the translation unit rather than per kernel. o2_gpu_add_kernel already walks the arguments in pairs, so it emits the index there. Declarations go through GPUPtr1(idx, type, name) for pointers and GPUArg1(idx, type, name) for scalars, which each backend defines as it needs. Indices start at 3, after gpu_mem, the constant memory and the sector; the sector itself is nothing special, it just lives in the fixed part of the macro rather than in the generated list. Metal masks pointers as a 64-bit address exactly as OpenCL does, and for the same reason: GPUTRDTrackerKernels takes a GPUTRDTrackerGPU*, and a pointer to a derived class is not a valid kernel argument type there either. Binding POD pointers directly would have worked but would not have covered that case, so both go the same way. On the way back in, GPUPtr2 casts through device before handing the pointer to Thread(): the kernel's own buffers are device memory, but the Thread() entry points take the pointer unannotated, which in MSL means generic. Generated entry points are byte-identical for CUDA, HIP and OpenCL. Kernel list diagnostics: 408 to 0, and the translation unit 1108 to 881.
| // through device and then to generic: the kernel's own buffers are device | ||
| // memory, but the Thread() entry points take the pointer unannotated | ||
| #define GPUPtr2(a, b) ((a)((device a)(b))) | ||
| #define GPUArg1(idx, a, b) constant a& b [[buffer(idx)]] |
There was a problem hiding this comment.
So in principle this is fine, if it is needed by METAL. In any case it is not exposed to the developed but hidden behind the scenes. However, I see 2 possible issues:
- You seem to number the buffers just counting up, but it could be that 2 of the pointers point to the same buffer. Would that be a problem?
- You define it as
constant a&, but there is no guarantee that the data passed in here is constant, the functions are allowed to modify it. What does constant mean here?
|
errors unrelated. merging. |
MSL has no by-value kernel parameters: every one must carry an attribute naming
a buffer or a builtin. It also has no ambient work-item builtins, so the grid
dimensions arrive as attributes on the entry point too.
GPUCA_KRNLGPU_DEF therefore gets two hooks, GPUCA_KRNL_SECTOR_ARG and
GPUCA_KRNL_GRID_ARGS, which the Metal source fills in with a buffer and the four
grid attributes. Both default to what the signature had, so CUDA, HIP and OpenCL
generate exactly the same entry point as before.
The attributes are named after the nBlocks, nThreads, iBlock and iThread that
Thread() already takes, so the get_*() helpers resolve at the entry point and in
everything it calls. Metal still needs its own definitions of them because the
host ones assume one thread per block: get_local_id() is 0 and get_local_size()
is 1 there.
The generated arguments need an attribute too, each with a distinct buffer index,
and the preprocessor cannot supply one: the kernel list splices arguments as a
flat comma-separated list, and COUNTER is monotonic across the translation
unit rather than per kernel. o2_gpu_add_kernel already walks the arguments in
pairs, so it emits the index there. Declarations go through GPUPtr1(idx, type,
name) for pointers and GPUArg1(idx, type, name) for scalars, which each backend
defines as it needs. Indices start at 3, after gpu_mem, the constant memory and
the sector; the sector itself is nothing special, it just lives in the fixed part
of the macro rather than in the generated list.
Metal masks pointers as a 64-bit address exactly as OpenCL does, and for the same
reason: GPUTRDTrackerKernels takes a GPUTRDTrackerGPU*, and a pointer to a
derived class is not a valid kernel argument type there either. Binding POD
pointers directly would have worked but would not have covered that case, so both
go the same way. On the way back in, GPUPtr2 casts through device before handing
the pointer to Thread(): the kernel's own buffers are device memory, but the
Thread() entry points take the pointer unannotated, which in MSL means generic.
Generated entry points are byte-identical for CUDA, HIP and OpenCL. Kernel list
diagnostics: 408 to 0, and the translation unit 1108 to 881.