Repository: kokkos/kokkos (2025-06-16T17:25:25Z)

Issue:
Cannot use SequentialHostInit with UnorderedMap
**Describe the bug**

Attempting to do:
```
UnorderedMap<int, int, HostSpace> map(view_alloc(SequentialHostInit), 4);
```
leads to compile errors because the `UnorderedMap` constructor appends `WithoutInitializing` to the property list passed to the constructor which hits a static assertion about not including both `WithoutInitializing` and `SequentialHostInit` in the same constructor properties.

**Please include the following for a minimal reproducer**
4.6.01 and develop both have the issue, can be reproduced with any compiler.

@crtrott @dalg24 


Discussion:
ok, since `UnorderedMap` internally uses types that are created `WithoutInitializing`, this clashes with the passed in `SequentialHostInit` even though it would work for the other data members of `UnorderedMap`.

We currently do not have the means to remove a property, thus we can not (simply) remove the `SequentialHostInit` for the members specified with `WithoutInitializing`. Adding this capability would be a big change.

What we can do is try to have the `SequentialHostInit` overwrite the `WithoutInitializing` sacrificing some performance
> What we can do is try to have the `SequentialHostInit` overwrite the `WithoutInitializing` sacrificing some performance

Yes, that's what I would do.
@vbrunini can you describe the use case you have? The documentation states UnorderedMap needs [trivially copyable types as values](https://kokkos.org/kokkos-core-wiki/API/containers/Unordered-Map.html#description). This would also impy that `SequentialHostInit` is not really necessary. If the value type is containing a `View` itself it would be sensical. @ldh4 said he saw a similar usecase. 
A use case that I encountered was an unordered map of classes containing multiple `DualView`s. Because this causes a similar scenario as a "view of view", `SequentialHostInit` seemed sensical to add. But as already described, this causes a static_assert failure, conflicting with internally specified `WithoutInitializing`.
@vbrunini let us know if this is a use case that you had in mind.
That's right the use case is where the value_type contains a `View`.
ok thanks for confirming the suspicion. Looking into if we can ease the restrictions on the type of `value` and if that is fine we can see which way we go to allow `SequentialHostInit` for `UnorderedMap`.
Just for reference, having types containing `View` as the value type of `UnorderedMap` has been working without issue since at least 4.1 (despite the documentation). Well post 4.4 required `IMPL_VIEW_OF_VIEWS_DESTRUCTOR_PRECONDITION_VIOLATION_WORKAROUND=ON`, but otherwise no issues. And I can confirm that your proposed workaround of overwriting the `WithoutInitializing` with `SequentialHostInit` works correctly with `IMPL_VIEW_OF_VIEWS_DESTRUCTOR_PRECONDITION_VIOLATION_WORKAROUND=OFF` now.
thanks for confirming, I got the same results. In our weekly meeting we were not decisive if we want to just drop adding the `WithoutInitialization` that we have for performance in that case or if we want to strip the `SequentialHostInit` for the members of `UnorderedMap` that we know will not need it.
Ok, IIUC using a `View` as `value_type` in `UnorderedMap` will be problematic as [`rehash` is potentially reallocating](https://github.com/kokkos/kokkos/blob/95494ae1e305884e064e9d159a560f9931eaf431/containers/src/) and at that point we don't have the properties that `UnorderedMap` got during its construction, so we just construct with the [defaults](https://github.com/kokkos/kokkos/blob/develop/containers/src/Kokkos_UnorderedMap.hpp#L449). @vbrunini @ldh4 do you have strong use cases why we should allow an `UnorderedMap` to be used with `value_type = View`? If so we can try to make it consistent but it definitely is an unintended usecase
Most of our use cases look are for something like `UnorderedMap<KeyType, variant<Function1, Function2, Function3>>` where one or more of the function options in the variant has one or more View members to store things like table data that the function interpolates within. Imagine something like a few different options for boundary condition types and which one gets applied depends on something about the portion of the surface currently being applied to. Generally these maps are small, and we know the exact size when we construct them, but being able to use the unordered map makes accessing them convenient.



Patch:
diff --git a/containers/src/Kokkos_UnorderedMap.hpp b/containers/src/Kokkos_UnorderedMap.hpp
index 12030ba11f1..95876516c3a 100644
--- a/containers/src/Kokkos_UnorderedMap.hpp
+++ b/containers/src/Kokkos_UnorderedMap.hpp
@@ -62,6 +62,39 @@
 
 namespace Kokkos {
 
+namespace Impl {
+
+template <typename ViewType, typename... P, typename... Args>
+auto allocate_without_initializing_if_possible(
+    const Impl::ViewCtorProp<P...> &alloc_prop, Args &&...args) {
+  using alloc_prop_t = Impl::remove_cvref_t<decltype(alloc_prop)>;
+
+  // if incompatible we don't add the property
+  if constexpr (alloc_prop_t::sequential_host_init)
+    return ViewType(alloc_prop, std::forward<Args>(args)...);
+  // otherwise we add it if unset
+  else
+    return ViewType(
+        Impl::with_properties_if_unset(alloc_prop, WithoutInitializing),
+        std::forward<Args>(args)...);
+}
+
+template <typename ViewType, typename... P, typename... Args>
+auto allocate_with_sequential_host_init_if_possible(
+    const Impl::ViewCtorProp<P...> &alloc_prop, Args &&...args) {
+  // if incompatible we don't add the property
+  if constexpr (!SpaceAccessibility<
+                    typename ViewType::execution_space::memory_space,
+                    HostSpace>::accessible)
+    return ViewType(alloc_prop, std::forward<Args>(args)...);
+  // otherwise we add it if unset
+  else
+    return ViewType(
+        Impl::with_properties_if_unset(alloc_prop, SequentialHostInit),
+        std::forward<Args>(args)...);
+}
+}  // namespace Impl
+
 enum : unsigned { UnorderedMapInvalidIndex = ~0u };
 
 /// \brief First element of the return value of UnorderedMap::insert().
@@ -331,7 +364,11 @@ class UnorderedMap {
   UnorderedMap(const Impl::ViewCtorProp<P...> &arg_prop,
                size_type capacity_hint = 0, hasher_type hasher = hasher_type(),
                equal_to_type equal_to = equal_to_type())
-      : m_bounded_insert(true), m_hasher(hasher), m_equal_to(equal_to) {
+      : m_bounded_insert(true),
+        m_hasher(hasher),
+        m_equal_to(equal_to),
+        m_sequential_host_init(
+            Impl::remove_cvref_t<decltype(arg_prop)>::sequential_host_init) {
     if (!is_insertable_map) {
       Kokkos::Impl::throw_runtime_exception(
           "Cannot construct a non-insertable (i.e. const key_type) "
@@ -339,7 +376,7 @@ class UnorderedMap {
     }
 
     //! Ensure that allocation properties are consistent.
-    using alloc_prop_t = std::decay_t<decltype(arg_prop)>;
+    using alloc_prop_t = Impl::remove_cvref_t<decltype(arg_prop)>;
     static_assert(alloc_prop_t::initialize,
                   "Allocation property 'initialize' should be true.");
     static_assert(
@@ -350,8 +387,6 @@ class UnorderedMap {
     /// properties.
     const auto prop_copy =
         Impl::with_properties_if_unset(arg_prop, std::string("UnorderedMap"));
-    const auto prop_copy_noinit =
-        Impl::with_properties_if_unset(prop_copy, Kokkos::WithoutInitializing);
 
     //! Initialize member views.
     m_size = shared_size_t(Kokkos::view_alloc(
@@ -362,14 +397,16 @@ class UnorderedMap {
         bitset_type(Kokkos::Impl::append_to_label(prop_copy, " - bitset"),
                     calculate_capacity(capacity_hint));
 
-    m_hash_lists = size_type_view(
-        Kokkos::Impl::append_to_label(prop_copy_noinit, " - hash list"),
-        Impl::find_hash_size(capacity()));
+    m_hash_lists =
+        Impl::allocate_without_initializing_if_possible<size_type_view>(
+            Kokkos::Impl::append_to_label(prop_copy, " - hash list"),
+            Impl::find_hash_size(capacity()));
 
-    m_next_index = size_type_view(
-        Kokkos::Impl::append_to_label(prop_copy_noinit, " - next index"),
-        capacity() + 1);  // +1 so that the *_at functions can always return a
-                          // valid reference
+    m_next_index =
+        Impl::allocate_without_initializing_if_possible<size_type_view>(
+            Kokkos::Impl::append_to_label(prop_copy, " - next index"),
+            capacity() + 1);  // +1 so that the *_at functions can always return
+                              // a valid reference
 
     m_keys = key_type_view(Kokkos::Impl::append_to_label(prop_copy, " - keys"),
                            capacity());
@@ -446,7 +483,12 @@ class UnorderedMap {
     requested_capacity =
         (requested_capacity < curr_size) ? curr_size : requested_capacity;
 
-    insertable_map_type tmp(requested_capacity, m_hasher, m_equal_to);
+    auto tmp =
+        m_sequential_host_init
+            ? Impl::allocate_with_sequential_host_init_if_possible<
+                  insertable_map_type>(view_alloc(), requested_capacity,
+                                       m_hasher, m_equal_to)
+            : insertable_map_type(requested_capacity, m_hasher, m_equal_to);
 
     if (curr_size) {
       tmp.m_bounded_insert = false;
@@ -807,7 +849,8 @@ class UnorderedMap {
         m_next_index(src.m_next_index),
         m_keys(src.m_keys),
         m_values(src.m_values),
-        m_scalars(src.m_scalars) {}
+        m_scalars(src.m_scalars),
+        m_sequential_host_init(src.m_sequential_host_init) {}
 
   template <typename SKey, typename SValue>
   std::enable_if_t<
@@ -815,16 +858,17 @@ class UnorderedMap {
                                   SValue>::value,
       declared_map_type &>
   operator=(UnorderedMap<SKey, SValue, Device, Hasher, EqualTo> const &src) {
-    m_bounded_insert    = src.m_bounded_insert;
-    m_hasher            = src.m_hasher;
-    m_equal_to          = src.m_equal_to;
-    m_size              = src.m_size;
-    m_available_indexes = src.m_available_indexes;
-    m_hash_lists        = src.m_hash_lists;
-    m_next_index        = src.m_next_index;
-    m_keys              = src.m_keys;
-    m_values            = src.m_values;
-    m_scalars           = src.m_scalars;
+    m_bounded_insert       = src.m_bounded_insert;
+    m_hasher               = src.m_hasher;
+    m_equal_to             = src.m_equal_to;
+    m_size                 = src.m_size;
+    m_available_indexes    = src.m_available_indexes;
+    m_hash_lists           = src.m_hash_lists;
+    m_next_index           = src.m_next_index;
+    m_keys                 = src.m_keys;
+    m_values               = src.m_values;
+    m_scalars              = src.m_scalars;
+    m_sequential_host_init = src.m_sequential_host_init;
     return *this;
   }
 
@@ -850,12 +894,14 @@ class UnorderedMap {
       UnorderedMap<SKey, SValue, SDevice, Hasher, EqualTo> const &src) {
     insertable_map_type tmp;
 
-    tmp.m_bounded_insert    = src.m_bounded_insert;
-    tmp.m_hasher            = src.m_hasher;
-    tmp.m_equal_to          = src.m_equal_to;
-    tmp.m_size()            = src.m_size();
-    tmp.m_available_indexes = bitset_type(src.capacity());
-    tmp.m_hash_lists        = size_type_view(
+    tmp.m_bounded_insert       = src.m_bounded_insert;
+    tmp.m_hasher               = src.m_hasher;
+    tmp.m_equal_to             = src.m_equal_to;
+    tmp.m_size()               = src.m_size();
+    tmp.m_sequential_host_init = src.m_sequential_host_init;
+    tmp.m_available_indexes    = bitset_type(src.capacity());
+
+    tmp.m_hash_lists = size_type_view(
         view_alloc(WithoutInitializing, "UnorderedMap hash list"),
         src.m_hash_lists.extent(0));
     tmp.m_next_index = size_type_view(
@@ -865,8 +911,13 @@ class UnorderedMap {
         key_type_view(view_alloc(WithoutInitializing, "UnorderedMap keys"),
                       src.m_keys.extent(0));
     tmp.m_values =
-        value_type_view(view_alloc(WithoutInitializing, "UnorderedMap values"),
-                        src.m_values.extent(0));
+        src.m_sequential_host_init
+            ? Impl::allocate_with_sequential_host_init_if_possible<
+                  value_type_view>(view_alloc("UnorderedMap values"),
+                                   src.m_values.extent(0))
+            : Impl::allocate_without_initializing_if_possible<value_type_view>(
+                  view_alloc("UnorderedMap values"), src.m_values.extent(0));
+
     tmp.m_scalars = scalars_view("UnorderedMap scalars");
 
     *this = tmp;
@@ -966,6 +1017,7 @@ class UnorderedMap {
   key_type_view m_keys;
   value_type_view m_values;
   scalars_view m_scalars;
+  bool m_sequential_host_init = false;
 
   template <typename KKey, typename VValue, typename DDevice, typename HHash,
             typename EEqualTo>


Test patch:
diff --git a/containers/unit_tests/TestUnorderedMap.hpp b/containers/unit_tests/TestUnorderedMap.hpp
index fc7435a75e5..43d26f53b7f 100644
--- a/containers/unit_tests/TestUnorderedMap.hpp
+++ b/containers/unit_tests/TestUnorderedMap.hpp
@@ -560,6 +560,48 @@ TEST(TEST_CATEGORY, UnorderedMap_constructor_view_alloc) {
   ASSERT_TRUE(map.is_allocated());
 }
 
+//////////////////////////Tests for UnorderedMap with View as value_type
+
+/**
+ * @test This test ensures that an @ref UnorderedMap with View as value_type can
+ * be built with SequentialHostInit instance (using @ref view_alloc).
+ */
+TEST(TEST_CATEGORY, UnorderedMap_View_as_value) {
+  using value_type = Kokkos::View<size_t *, TEST_EXECSPACE>;
+  using map_type   = Kokkos::UnorderedMap<int, value_type, Kokkos::HostSpace>;
+  map_type map(Kokkos::view_alloc(Kokkos::SequentialHostInit, "test view umap"),
+               150);
+  // creation
+  ASSERT_EQ(map.size(), 0u);
+  ASSERT_GE(map.capacity(), 150u);
+  ASSERT_TRUE(map.is_allocated());
+
+  // insert
+  ASSERT_TRUE(map.insert(1, Kokkos::View<size_t *, TEST_EXECSPACE>(
+                                "UnorderedMap inserted View one", 10))
+                  .success());
+  ASSERT_TRUE(map.insert(2, Kokkos::View<size_t *, TEST_EXECSPACE>(
+                                "UnorderedMap inserted View two", 20))
+                  .success());
+  ASSERT_EQ(map.size(), 2u);
+
+  // copy
+  map_type map_copy(map);
+  ASSERT_EQ(map_copy.size(), 2u);
+  ASSERT_GE(map_copy.capacity(), 150u);
+  ASSERT_TRUE(map_copy.is_allocated());
+
+  // rehash
+  ASSERT_TRUE(map.rehash(200u));
+  ASSERT_GE(map.capacity(), 200u);
+  ASSERT_TRUE(map.is_allocated());
+
+  // assign
+  map_copy = map;
+  ASSERT_EQ(map_copy.size(), 2u);
+  ASSERT_GE(map_copy.capacity(), 200u);
+  ASSERT_TRUE(map_copy.is_allocated());
+}
 }  // namespace Test
 
 #endif  // KOKKOS_TEST_UNORDERED_MAP_HPP