Device-wide chained scan with lookback - #1086
Conversation
…probably not needed but left in for now
| return spirv::atomicSMin<T>(ptr, spv::ScopeDevice, spv::MemorySemanticsMaskNone, value); | ||
| } | ||
|
|
||
| template<typename Ptr_T> // DXC Workaround |
There was a problem hiding this comment.
It's best to say exactly what dxc issue we're working around and be more detailed in the comments. in case they fix it and we can upgrade our code.
I think it's something to do with passing groupshared or other address qualifiers here.
I have a question though.
These don't need to be cpp compatible, it's gpu specific, why are we using NBL_REF_ARG and both REQ_TOP and REQ_BOT (hlsl enable_if and c++20 requires). I think BOT is enough?
Side note:
Just putting it out there that I don't like TOP/BOT naming 😆 I'd prefer something along the lines of:
NBL_HOST_CONCEPT+NBL_DEVICE_CONCEPTorNBL_DEVICE_SFINAE. which is more clear
There was a problem hiding this comment.
These don't need to be cpp compatible, it's gpu specific, why are we using NBL_REF_ARG and both REQ_TOP and REQ_BOT (hlsl enable_if and c++20 requires). I think BOT is enough?
Because in C++20 you need the requires clause on the top, but enable_if workaround needs it in the bottom.
Thats the convention we already chose and if we were to change it we'd have to change a lot of other code as well.
There was a problem hiding this comment.
I have a question though.
These don't need to be cpp compatible, it's gpu specific, why are we using NBL_REF_ARG and both REQ_TOP and REQ_BOT (hlsl enable_if and c++20 requires). I think BOT is enough?
you want to be forward compatible with C++20, because HLSL is going that way, also it keeps the code easy to port to CUDA and SYCL later on.
There was a problem hiding this comment.
It's best to say exactly what dxc issue we're working around and be more detailed in the comments. in case they fix it and we can upgrade our code.
I think it's something to do with passing groupshared or other address qualifiers here.
Yes @keptsecret please cite/link the DXC issue in the comments for stuff like that (git commit message too)
| void __call(NBL_REF_ARG(DataAccessor) dataAccessor, NBL_REF_ARG(ScratchAccessor) scratchAccessor, NBL_REF_ARG(ReductionAccessor) workgroupReduction, NBL_REF_ARG(WorkgroupCounter) workgroupCounter) | ||
| { | ||
| const uint16_t invocIx = workgroup::SubgroupContiguousIndex(); | ||
| if (!invocIx) |
There was a problem hiding this comment.
For readability purposes if you want to check if something is equal to zero, it's best to just do ==0 instead of treating as a boolean
|
|
||
| uint16_t workgroupId; | ||
| scratchAccessor.template get<uint32_t, uint32_t>(0u, workgroupId); | ||
| scratchAccessor.workgroupExecutionAndMemoryBarrier(); |
There was a problem hiding this comment.
you don't need a workgroupExecutionAndMemoryBarrier here.
| workgroup2::exclusive_scan<Config,BinOp,device_capabilities>::template __call<wg_data_proxy_t, ScratchAccessor>(wgDataAccessor, scratchAccessor); | ||
| else | ||
| workgroup2::inclusive_scan<Config,BinOp,device_capabilities>::template __call<wg_data_proxy_t, ScratchAccessor>(wgDataAccessor, scratchAccessor); | ||
| scratchAccessor.workgroupExecutionAndMemoryBarrier(); |
There was a problem hiding this comment.
I don't think you need a workgroupExecutionAndMemoryBarrier Because you want to access the preloaded array which is thread local.
| currGroupReduction = wgDataAccessor.preloaded[wg_data_proxy_t::PreloadedDataCount-1u][Config::ItemsPerInvocation_0-1u]; | ||
| if (Exclusive) | ||
| currGroupReduction = binop(currGroupReduction, lastElem); | ||
| if (invocIx == lastInvocIx) | ||
| scratchAccessor.template set<scalar_t, uint32_t>(0u, currGroupReduction); |
There was a problem hiding this comment.
You only need to do this for the last invocation.
So maybe you can encompass all of it under if (invocIx == lastInvocIx)
| if (workgroupId) | ||
| { | ||
| bool locked = sIsLocked; | ||
| scratchAccessor.workgroupExecutionAndMemoryBarrier(); |
There was a problem hiding this comment.
why do workgroups need to sync here?
| scratchAccessor.workgroupExecutionAndMemoryBarrier(); | ||
|
|
||
| locked = sIsLocked; | ||
| scratchAccessor.workgroupExecutionAndMemoryBarrier(); |
There was a problem hiding this comment.
why workgroupExecutionAndMemoryBarrier here ?
|
|
||
| locked = sIsLocked; | ||
| scratchAccessor.workgroupExecutionAndMemoryBarrier(); | ||
| if (locked) |
There was a problem hiding this comment.
Add comment:
Fall back path: we spun to MaxSpinCount But no previous workgroup had there global reduction ready (Flag_Inclusive).
So we try to do reduction for all previous work groups one by one until we reach Flag_Inclusive
| const scalar_t storeVal = hlsl::mix(Flag_Inclusive, Flag_Reduction, fallbackGroupId > 0u) | (fallbackReduction << Flag_Shift); | ||
| const scalar_t fallbackPayload = workgroupReduction.atomicMax(fallbackGroupId, storeVal); | ||
|
|
||
| prevReduction = binop(prevReduction, hlsl::mix(fallbackReduction, fallbackPayload >> Flag_Shift, fallbackPayload > scalar_t(0.0))); |
There was a problem hiding this comment.
I get why you're doing atomic Max here, You want to take the inclusive one if the original workgroup Finished after we calculated the reduction redundantly
But I don't get the mix here. Why not take the fall back payload all the time?
| if (fallbackGroupId == 0u || (fallbackPayload & Flag_Mask) == Flag_Inclusive) | ||
| { | ||
| const scalar_t storeVal = Flag_Inclusive | (binop(prevReduction, currGroupReduction) << Flag_Shift); | ||
| workgroupReduction.atomicExchange(workgroupId, storeVal); |
There was a problem hiding this comment.
Be careful again, you need some sort of memory semantics to ensure correct memory barriers when different work groups access the same value atomically.
| scratchAccessor.workgroupExecutionAndMemoryBarrier(); | ||
|
|
||
| locked = sIsLocked; | ||
| scratchAccessor.workgroupExecutionAndMemoryBarrier(); |
There was a problem hiding this comment.
why workgroupExecutionAndMemoryBarrier here ?
| wg_data_proxy_t fallbackDataAccessor = wg_data_proxy_t::create(dataAccessor.getInputBufAddr(), dataAccessor.getOutputBufAddr(), fallbackGroupId); | ||
| fallbackDataAccessor.preload(); | ||
| scalar_t fallbackReduction = workgroup2::reduction<Config,BinOp,device_capabilities>::template __call<wg_data_proxy_t, ScratchAccessor>(fallbackDataAccessor, scratchAccessor); | ||
| scratchAccessor.workgroupExecutionAndMemoryBarrier(); |
There was a problem hiding this comment.
why workgroupExecutionAndMemoryBarrier here ?
| template<typename T, typename V, typename I=uint32_t> | ||
| NBL_BOOL_CONCEPT ArithmeticSharedMemoryAccessor = concepts::accessors::GenericSharedMemoryAccessor<T,V,I>; | ||
|
|
||
| template<typename T, typename V, typename I=uint32_t> | ||
| NBL_BOOL_CONCEPT ArithmeticReadOnlyDataAccessor = concepts::accessors::GenericReadAccessor<T,V,I>; | ||
|
|
||
| template<typename T, typename V, typename I=uint32_t> | ||
| NBL_BOOL_CONCEPT ArithmeticDataAccessor = concepts::accessors::GenericDataAccessor<T,V,I>; |
There was a problem hiding this comment.
why are we introducing new aliases?
| #define NBL_CONCEPT_NAME DeviceReductionsAccessor | ||
| #define NBL_CONCEPT_TPLT_PRM_KINDS (typename)(typename) | ||
| #define NBL_CONCEPT_TPLT_PRM_NAMES (T)(V) | ||
| #define NBL_CONCEPT_PARAM_0 (accessor, T) | ||
| #define NBL_CONCEPT_PARAM_1 (val, V) | ||
| #define NBL_CONCEPT_PARAM_2 (index, uint64_t) | ||
| NBL_CONCEPT_BEGIN(3) | ||
| #define accessor NBL_CONCEPT_PARAM_T NBL_CONCEPT_PARAM_0 | ||
| #define val NBL_CONCEPT_PARAM_T NBL_CONCEPT_PARAM_1 | ||
| #define index NBL_CONCEPT_PARAM_T NBL_CONCEPT_PARAM_2 | ||
| NBL_CONCEPT_END( | ||
| ((NBL_CONCEPT_REQ_TYPE_ALIAS_CONCEPT)(concepts::accessors::GenericDataAccessor, T, V, uint64_t)) | ||
| ((NBL_CONCEPT_REQ_EXPR_RET_TYPE)((accessor.atomicMax(index, val)), is_same_v, V)) | ||
| ((NBL_CONCEPT_REQ_EXPR_RET_TYPE)((accessor.atomicExchange(index, val)), is_same_v, V)) | ||
| ); | ||
| #undef val | ||
| #undef index | ||
| #undef accessor | ||
| #include <nbl/builtin/hlsl/concepts/__end.hlsl> | ||
|
|
There was a problem hiding this comment.
shall we generalize more and hoist it out into a GenericAtomicAccessor (no arithmetic)
| #undef accessor | ||
| #include <nbl/builtin/hlsl/concepts/__end.hlsl> | ||
|
|
||
| // TODO: as counter, maybe just increment 1 always? |
There was a problem hiding this comment.
nah don't bother
| #define NBL_CONCEPT_NAME WorkgroupCounterAccessor | ||
| #define NBL_CONCEPT_TPLT_PRM_KINDS (typename) | ||
| #define NBL_CONCEPT_TPLT_PRM_NAMES (T) | ||
| #define NBL_CONCEPT_PARAM_0 (accessor, T) | ||
| #define NBL_CONCEPT_PARAM_1 (val, uint32_t) | ||
| #define NBL_CONCEPT_PARAM_2 (index, uint64_t) | ||
| NBL_CONCEPT_BEGIN(3) | ||
| #define accessor NBL_CONCEPT_PARAM_T NBL_CONCEPT_PARAM_0 | ||
| #define val NBL_CONCEPT_PARAM_T NBL_CONCEPT_PARAM_1 | ||
| #define index NBL_CONCEPT_PARAM_T NBL_CONCEPT_PARAM_2 | ||
| NBL_CONCEPT_END( | ||
| ((NBL_CONCEPT_REQ_EXPR_RET_TYPE)((accessor.atomicAdd(index, val)), is_same_v, uint32_t)) | ||
| ); | ||
| #undef val | ||
| #undef index | ||
| #undef accessor | ||
| #include <nbl/builtin/hlsl/concepts/__end.hlsl> |
There was a problem hiding this comment.
same could generalize to GenericAtomicIntAccessor or GenericAtomicArithmeticAccessor
| namespace impl | ||
| { | ||
| template<typename T NBL_STRUCT_CONSTRAINABLE> | ||
| struct atomicMin; | ||
|
|
||
| template<typename T> | ||
| NBL_PARTIAL_REQ_TOP(concepts::SignedIntegral<T>) | ||
| struct atomicMin<T NBL_PARTIAL_REQ_BOT(concepts::SignedIntegral<T>) > | ||
| { | ||
| static T __call(NBL_REF_ARG(T) ptr, T value) | ||
| { | ||
| return spirv::atomicSMin<T>(ptr, spv::ScopeDevice, spv::MemorySemanticsMaskNone, value); | ||
| } | ||
|
|
||
| template<typename Ptr_T> // DXC Workaround | ||
| static T __call(Ptr_T ptr, T value) | ||
| { | ||
| return spirv::atomicSMin<T>(ptr, spv::ScopeDevice, spv::MemorySemanticsMaskNone, value); | ||
| } | ||
| }; | ||
|
|
||
| template<typename T> | ||
| NBL_PARTIAL_REQ_TOP(concepts::UnsignedIntegral<T>) | ||
| struct atomicMin<T NBL_PARTIAL_REQ_BOT(concepts::UnsignedIntegral<T>) > | ||
| { | ||
| static T __call(NBL_REF_ARG(T) ptr, T value) | ||
| { | ||
| return spirv::atomicUMin<T>(ptr, spv::ScopeDevice, spv::MemorySemanticsMaskNone, value); | ||
| } | ||
|
|
||
| template<typename Ptr_T> // DXC Workaround | ||
| static T __call(Ptr_T ptr, T value) | ||
| { | ||
| return spirv::atomicUMin<T>(ptr, spv::ScopeDevice, spv::MemorySemanticsMaskNone, value); | ||
| } | ||
| }; | ||
|
|
||
| template<typename T NBL_STRUCT_CONSTRAINABLE> | ||
| struct atomicMax; | ||
|
|
||
| template<typename T> | ||
| NBL_PARTIAL_REQ_TOP(concepts::SignedIntegral<T>) | ||
| struct atomicMax<T NBL_PARTIAL_REQ_BOT(concepts::SignedIntegral<T>) > | ||
| { | ||
| static T __call(NBL_REF_ARG(T) ptr, T value) | ||
| { | ||
| return spirv::atomicSMax<T>(ptr, spv::ScopeDevice, spv::MemorySemanticsMaskNone, value); | ||
| } | ||
|
|
||
| template<typename Ptr_T> // DXC Workaround | ||
| static T __call(Ptr_T ptr, T value) | ||
| { | ||
| return spirv::atomicSMax<T>(ptr, spv::ScopeDevice, spv::MemorySemanticsMaskNone, value); | ||
| } | ||
| }; | ||
|
|
||
| template<typename T> | ||
| NBL_PARTIAL_REQ_TOP(concepts::UnsignedIntegral<T>) | ||
| struct atomicMax<T NBL_PARTIAL_REQ_BOT(concepts::UnsignedIntegral<T>) > | ||
| { | ||
| static T __call(NBL_REF_ARG(T) ptr, T value) | ||
| { | ||
| return spirv::atomicUMax<T>(ptr, spv::ScopeDevice, spv::MemorySemanticsMaskNone, value); | ||
| } | ||
|
|
||
| template<typename Ptr_T> // DXC Workaround | ||
| static T __call(Ptr_T ptr, T value) | ||
| { | ||
| return spirv::atomicUMax<T>(ptr, spv::ScopeDevice, spv::MemorySemanticsMaskNone, value); | ||
| } | ||
| }; | ||
| } |
There was a problem hiding this comment.
we actually have a huge problem here and I think this is why I didn't pour that much effort into the GLSL compat functions
- You don't know the address space so don't know the scope to emit by default - comment at the top of the atomic code block
- memory semantics are always None
Ideally none of our code should be using the GLSL atomics because they're so bad and imprecise
No description provided.