Skip to content

Device-wide chained scan with lookback - #1086

Open
keptsecret wants to merge 22 commits into
masterfrom
device_chained_scan
Open

Device-wide chained scan with lookback#1086
keptsecret wants to merge 22 commits into
masterfrom
device_chained_scan

Conversation

@keptsecret

Copy link
Copy Markdown
Contributor

No description provided.

return spirv::atomicSMin<T>(ptr, spv::ScopeDevice, spv::MemorySemanticsMaskNone, value);
}

template<typename Ptr_T> // DXC Workaround

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_CONCEPT or NBL_DEVICE_SFINAE. which is more clear

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seconded.


uint16_t workgroupId;
scratchAccessor.template get<uint32_t, uint32_t>(0u, workgroupId);
scratchAccessor.workgroupExecutionAndMemoryBarrier();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you need a workgroupExecutionAndMemoryBarrier Because you want to access the preloaded array which is thread local.

Comment on lines +118 to +122
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do workgroups need to sync here?

scratchAccessor.workgroupExecutionAndMemoryBarrier();

locked = sIsLocked;
scratchAccessor.workgroupExecutionAndMemoryBarrier();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why workgroupExecutionAndMemoryBarrier here ?


locked = sIsLocked;
scratchAccessor.workgroupExecutionAndMemoryBarrier();
if (locked)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seconded

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)));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be careful again, you need some sort of memory semantics to ensure correct memory barriers when different work groups access the same value atomically.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seconded

scratchAccessor.workgroupExecutionAndMemoryBarrier();

locked = sIsLocked;
scratchAccessor.workgroupExecutionAndMemoryBarrier();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why workgroupExecutionAndMemoryBarrier here ?

Comment on lines +13 to +20
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>;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we introducing new aliases?

Comment on lines +22 to +41
#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>

@devshgraphicsprogramming devshgraphicsprogramming Aug 24, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nah don't bother

Comment on lines +43 to +59
#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>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same could generalize to GenericAtomicIntAccessor or GenericAtomicArithmeticAccessor

Comment on lines +140 to +211
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);
}
};
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. 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
  2. memory semantics are always None

Ideally none of our code should be using the GLSL atomics because they're so bad and imprecise

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants