1
\$\begingroup\$
//MEM = initial memory, INDATA, the actual floating point value
#define atomicAddFloatX(MEM, INDATA)\
{\
    float data = INDATA;\
    float prev_mem = uintBitsToFloat(MEM);\
    float exchange_mem = prev_mem;\
    float input_mem = (data + prev_mem);\  //initially this is what we assume we want to put in here. 
    while (true){\
        uint exchange_memuint = atomicExchange(MEM, floatBitsToUint(input_mem));\ //exchange with memory location, assume other threads are doing the same exact thing. atomic exchange returns the value that was already there. 
        exchange_mem = uintBitsToFloat(exchange_memuint);\ //get the floating point value from returned value. 
        if (prev_mem != exchange_mem){\  //if it was equal to what we initially suspected, we can exit, our data is guaranteed to be in this memory location for all other threads trying to perform this function. 
            input_mem = exchange_mem + data;\ //otherwise we need to update our input. Another thread completed their atomic exchange before us. 
            prev_mem = exchange_mem;\ // update our expectation of return now. 
        } else {\
            break;\
        }\
    }\
}

//example
atomicAddFloatX(output[thread_id].x,  3.0);

Vulkan supports atomic float, but only for Nvidia at the moment. It's possible however that other vendors will never support atomic float. In order to provide atomic float functionality, which is absolutely required for some algorithms, I tried my best to try to create something that would have the same effect, if less performant.

I have access to #extension GL_KHR_memory_scope_semantics, but it is unclear if that can help me or not. I think I have bugs in how I'm dealing with loading the initial memory logically, though through internal tests in projects where this is used this seems to not have errors, but that may be a fluke of the Nvidia memory model. I'm currently using this for the immersed boundary method for fluid solid coupling, its used to accumulate forces in a part of the algorithm.

I'm also not sure if there's other things I can do to speed this up, though I think this has serialized performance characteristics, which should be equivalent to atomic float normally.

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

I did not realize what atomicCompSwap did nor that it was common for this kind of usecase: https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/atomicCompSwap.xhtml . This function handles the case of the loaded value for MEM being read incorrectly, this check is automatically handled via the function itself, and simplifies the code. Using the original value of memory we can verify if our assumptions about what we should be adding to are correct:

#define atomicAddFloatX(MEM, INDATA)      \
{                                         \
    uint expected_mem = MEM;              \
    float input_mem = (uintBitsToFloat(MEM) + INDATA); \  //initially this is what we assume we want to put in here. 
    uint returned_mem = atomicCompSwap(MEM, expected_mem, floatBitsToUint(input_mem)); \ //if data returned is what we expected it to be, we're good, we added to it successfully
    while(returned_mem != expected_mem){ \ // if the data returned is something different, we know another thread completed its transaction, so we'll have to add to that instead. 
        expected_mem = returned_mem; \
        input_mem = (uintBitsToFloat(expected_mem) + INDATA) \
        returned_mem = atomicCompSwap(MEM, expected_mem, floatBitsToUint(input_mem)); \
    } \
}

Basically the code I wrote before was attempting to do something like this, but more confusing and a much less safe way.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.