Skip to content

Proposal load/store masked#1162

Closed
DiamonDinoia wants to merge 1 commit into
xtensor-stack:masterfrom
DiamonDinoia:masked-memory-ops
Closed

Proposal load/store masked#1162
DiamonDinoia wants to merge 1 commit into
xtensor-stack:masterfrom
DiamonDinoia:masked-memory-ops

Conversation

@DiamonDinoia
Copy link
Copy Markdown
Contributor

Dear @serge-sans-paille,

I made a rough implementation of masked load/store. Before I hash it out. Can I have some early feedback?

Thanks,
Marco

@serge-sans-paille
Copy link
Copy Markdown
Contributor

interesting. Before going into the details, what are the memory effects of a masked load in terms of read memory and value stored? Are the masked elements set to 0 ? to an undefined value ? AVX etc seems to set the value to zero?

I'm not sold on the common implementation which looks quite heavy in scalar operation. I can see that we can't do a plain load followed by an and because it could lead to access to unallocated memory. If the mask were constant, we could optimize statically some common patterns, but with a dynamic mask as you propose...

@DiamonDinoia
Copy link
Copy Markdown
Contributor Author

DiamonDinoia commented Aug 22, 2025

Some thoughts:

  1. Undefined for masked values. Since depending on the operations 0 or 1 might be the correct values. In that case they could use the mask itself to initialize the values. Also, because imagine I want to polulate the even elements from one memory location and the odd from another. Masked loads (I think) are faster than a gather. set to 0 following the x86 convention
  2. We could remove the dynamic mask entirely. I added for completeness.
  3. We could do a la vcl and have a load partial, store partial where we just optimize for head and tail. I preferred this solution as I'm assuming xsimd users know the performance implications of the API.
  4. For now this is fast only on avx, av2 but for sse even if it heavy on scalar it is slow only when reading bytes or short. We could optimize these cases.
  5. I'm not sure about sve/neon
  6. With static masks if the first and the last element are read it is possible to do load+and

In general, I use these operations when I want to vectorize and inner loop that is not a multiple of the simd width. This is a small inner loop nested in a loop executed a lot of times. Depending on the operations, padding sometimes is slower than masking.

@DiamonDinoia
Copy link
Copy Markdown
Contributor Author

Hi @serge-sans-paille,

This is starting to take shape now. I still need to clean up. But this is a first working implementation.

I extended the mask operation in batch_bool to mimic std::bit so that i could reuse the code for all masked operations. I feel having those living outside the class in detail:: is not the best. They are also useful to the user I think.

@DiamonDinoia DiamonDinoia force-pushed the masked-memory-ops branch 8 times, most recently from fd0e777 to ecf4291 Compare October 9, 2025 23:27
@DiamonDinoia
Copy link
Copy Markdown
Contributor Author

@serge-sans-paille do you have any clue on why mingw32 fails?

I need to do a final pass make the code coherent (I am mixing reinterpret_casts and c-style casts). Affter that I think will ready for review.

}
else
{
// Fallback to runtime path for non prefix/suffix masks
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.

Unless the compiler optimizes those out, the dynamic load is going to perform serveral checks that are going to be always false (none / all).
I'm still not convinced by the relevancy of dynamic mask support. Maybe we could start small with "just" the static mask support and eventually add a dynamic version later if it appears to be needed? But maybe you have specific case in mind ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It is just that in my mind I can offer a public runtime mask API
then the compile time mask optimizes as much as it can and if there is no alternative it calls the runtime mask.

Same as the swizzle. So I was not planning of optimizing the runtime at all we just get a maskl_load/store where possible or a common since sse does not support masking.

PS:
This can be optimized in the future with using avx on sse or avx512vl but I will open a discussion once this is merged

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I am cleaning this to only offer compile time for now and the runtime can be a separate PR

@DiamonDinoia DiamonDinoia force-pushed the masked-memory-ops branch 11 times, most recently from e535375 to 4c457ae Compare October 19, 2025 02:18
@DiamonDinoia DiamonDinoia marked this pull request as ready for review October 19, 2025 04:02
@DiamonDinoia
Copy link
Copy Markdown
Contributor Author

I don't think the error is due to anything related to this PR. But, I like the looks of it now. Ready to review!

@serge-sans-paille
Copy link
Copy Markdown
Contributor

@DiamonDinoia we plan to do a release in the forthcoming weeks. Do you want this PR to be part of it?

@DiamonDinoia
Copy link
Copy Markdown
Contributor Author

@DiamonDinoia we plan to do a release in the forthcoming weeks. Do you want this PR to be part of it?

Only if it does not delay the release by much. I would like to have a release by mid-November. This gives me two weeks to update my downstream projects. For an end of the year release.

PS: I am planning on addressing the swizzle issue after this PR.

@JohanMabille
Copy link
Copy Markdown
Member

I would like to have a release by mid-November

That is more or less what we planned ;)

Copy link
Copy Markdown
Contributor

@serge-sans-paille serge-sans-paille left a comment

Choose a reason for hiding this comment

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

A new wave of comments. I have the feeling that we could simplify the implementation a lot by making it more generic.

Comment thread include/xsimd/arch/common/xsimd_common_arithmetic.hpp
Comment thread include/xsimd/arch/xsimd_avx2.hpp Outdated
return load<A>(mem, Mode {});
}
// confined to lower 128-bit half (4 lanes) → forward to SSE
else XSIMD_IF_CONSTEXPR(mask.countl_zero() >= 4)
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.

Thinking of it, we could remove the need for countl_zero by just masking, something like

if((mask.mask() & 0xF0) == 0)

which would be a decent implementation for

if(mask.lower_half() == 0) // or lower_mask ?

which has the nice property of not encoding any "magical value"

Copy link
Copy Markdown
Contributor Author

@DiamonDinoia DiamonDinoia Nov 5, 2025

Choose a reason for hiding this comment

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

The reason why I went for countl_zero is to maintain parity with std::bit. I feel like this is public API so it is worth for users not having to learn a new naming scheme

Comment thread include/xsimd/arch/xsimd_avx.hpp Outdated
else XSIMD_IF_CONSTEXPR(mask.countr_zero() >= 2)
{
constexpr auto mhi = ::xsimd::detail::upper_half<sse2>(mask);
const batch<double, sse2> hi(_mm256_extractf128_pd(src, 1));
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.

that's a upper_half, right?

Comment thread include/xsimd/arch/xsimd_avx.hpp Outdated
else XSIMD_IF_CONSTEXPR(mask.countl_zero() >= 2)
{
constexpr auto mlo = ::xsimd::detail::lower_half<sse2>(mask);
const batch<double, sse2> lo(_mm256_castpd256_pd128(src));
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.

that's a lower_half, right?

{
_mm256_maskstore_pd(mem, mask.as_batch(), src);
}
}
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.

epiphany : if you express everything in terms of lower / upper half, you're pretty close to a type-generic implementation that would avoid a lot of redunduncy. just need to wrap the actual _mm256_maskstore* and we're good, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I guess you are right. It might be possible to express things in terms of size and halves. This might be recursive though and we might need to arch to half arch, similarly to what happens in widen.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'll also have to think if the size of the element matters. I'll have a look next week

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The reason why I kept the implementation separate is as follows:

The general implementation in terms of upper/lower ais possible. This haves the code in avx, avx2 still has to provide a new implementation so that it can take advantage of the new intrinsics so we for from 2 versions in avx down to 1 and 2 in avx2 down to 1. Same for avx512.

But then for merging we can not use more optimized intrinsics than _mm256_castps128_ps256. Is it worth it?

The solution here is to pass the MaskLoad/Store and the merge function as templates to this general implementation. Then we could have 1 general implementation maybe for all the kernels. But, I think it might be too much templating.

In general also wrapping _mm256_maskstore* also makes a clean way to expose this to the used if we wish to do so.

What do you suggest?

Comment thread include/xsimd/arch/xsimd_avx512f.hpp Outdated
return _mm512_loadu_pd(mem);
}

// load_masked
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 did you split the load_masked in two parts of the header? The generic epiphany for avx seems to apply here too :-)

Comment thread include/xsimd/arch/xsimd_avx.hpp Outdated
{
constexpr auto mlo = ::xsimd::detail::lower_half<sse4_2>(mask);
const auto lo = load_masked(mem, mlo, convert<float> {}, Mode {}, sse4_2 {});
return batch<float, A>(detail::merge_sse(lo, batch<float, sse4_2>(0.f)));
Copy link
Copy Markdown
Contributor

@serge-sans-paille serge-sans-paille Oct 31, 2025

Choose a reason for hiding this comment

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

could be a call to _mm256_castps128_ps256 instead of merge + batch of zero

Comment thread include/xsimd/arch/xsimd_avx.hpp Outdated
{
constexpr auto mhi = ::xsimd::detail::upper_half<sse4_2>(mask);
const auto hi = load_masked(mem + 4, mhi, convert<float> {}, Mode {}, sse4_2 {});
return batch<float, A>(detail::merge_sse(batch<float, sse4_2>(0.f), hi));
Copy link
Copy Markdown
Contributor

@serge-sans-paille serge-sans-paille Oct 31, 2025

Choose a reason for hiding this comment

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

I don't know if we could use a similar trick to _mm256_castps128_ps256 here

@serge-sans-paille
Copy link
Copy Markdown
Contributor

serge-sans-paille commented Nov 3, 2025

@AntoinePrv once you update that branch we can do a release \o/
EDIT: I meant @DiamonDinoia of course :-)

@DiamonDinoia
Copy link
Copy Markdown
Contributor Author

@AntoinePrv once you update that branch we can do a release \o/

I will work on this asap. I just have something to deliver on another project this week so it might take a bit for me to get back to this.

@serge-sans-paille
Copy link
Copy Markdown
Contributor

Note that once you're done with Intel support, i still have to implement ARM et cie when possible

@DiamonDinoia DiamonDinoia force-pushed the masked-memory-ops branch 7 times, most recently from 856d98e to 9185bb2 Compare November 13, 2025 22:46
@DiamonDinoia
Copy link
Copy Markdown
Contributor Author

I removed most of the duplication at least I stopped when I thought things were getting out of hand. I think in the future we might offer direct access to masked load with dynamic mask. Which is basically exposing the internal helper functions but adapting them to the xsimd convention.

1. Adds new masked API compile time masks (store_masked and load_masked)
2. General use case optimization
3. New tests
4. x86 kernels
5. Adds new APIs to batch_bool_constant for convenience resembling #include<bit>
6. Tests the new APIs
@serge-sans-paille
Copy link
Copy Markdown
Contributor

Merged as 239fbbd
Thanks a lot @DiamonDinoia for the hard work. It's now on me to implement part of it for other architectures.

Comment on lines +162 to +166
template <class A, bool... Values, class Mode>
XSIMD_INLINE batch<int32_t, A> load_masked(int32_t const* mem, batch_bool_constant<int32_t, A, Values...> mask, convert<int32_t>, Mode, requires_arch<avx2>) noexcept
{
return load_masked<A>(mem, mask, convert<int32_t> {}, Mode {}, avx2 {});
}
Copy link
Copy Markdown
Contributor

@AntoinePrv AntoinePrv Nov 19, 2025

Choose a reason for hiding this comment

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

@DiamonDinoia I am having trouble with this function above. Is this an infinite loop?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I might have meant calling uint -> int32 there.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I am currently travelling. I have a look once I am back.

@AntoinePrv AntoinePrv mentioned this pull request Nov 19, 2025
4 tasks
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.

4 participants