diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt
index 4a50799e8f99a005de5969996af1ffc6170449a0..71cbd392a0d9fe26a6b5a0240ca07915eab5d881 100644
--- a/lib/CMakeLists.txt
+++ b/lib/CMakeLists.txt
@@ -7,6 +7,7 @@ target_sources( walberla_experimental
     walberla/experimental/sweep/SparseIndexList.hpp
     walberla/experimental/lbm/GenericHbbBoundary.hpp
     walberla/experimental/lbm/IrregularFreeSlip.hpp
+    walberla/experimental/communication/UniformGpuFieldPackInfoBase.hpp
 )
 
 target_link_libraries(
diff --git a/lib/walberla/experimental/communication/UniformGpuFieldPackInfoBase.hpp b/lib/walberla/experimental/communication/UniformGpuFieldPackInfoBase.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..803455e449f5d8673de519883ebcf128fbdd0c41
--- /dev/null
+++ b/lib/walberla/experimental/communication/UniformGpuFieldPackInfoBase.hpp
@@ -0,0 +1,87 @@
+#pragma once
+
+// #if defined(WALBERLA_BUILD_WITH_CUDA) || defined(WALBERLA_BUILD_WITH_HIP)
+
+#include "core/all.h"
+
+#include "gpu/communication/GeneratedGPUPackInfo.h"
+
+#include <concepts>
+#include <span>
+
+namespace walberla::experimental::communication
+{
+
+namespace detail
+{
+template< typename T >
+concept UniformGpuFieldPackInfoImpl = requires(T impl,                                              //
+                                               typename T::Field_T& field,                          //
+                                               std::span< typename T::Field_T::value_type > buffer, //
+                                               stencil::Direction dir,                              //
+                                               CellInterval& ci,                                    //
+                                               gpuStream_t stream                                   //
+) {
+   typename T::Field_T;
+
+   { impl.doPack(field, buffer, dir, ci, stream) } -> std::same_as< void >;
+
+   { impl.doUnpack(field, buffer, dir, ci, stream) } -> std::same_as< void >;
+
+   { impl.elementsPerCell(dir) } -> std::same_as< uint_t >;
+}
+} // namespace detail
+
+template< detail::UniformGpuFieldPackInfoImpl Impl >
+class UniformGpuFieldPackInfoBase : public gpu::GeneratedGPUPackInfo
+{
+ public:
+   UniformGpuFieldPackInfoBase(BlockDataID fieldId, uint_t sliceWidth = 0)
+      : fieldId_{ fieldId }, sliceWidth_{ sliceWidth }
+   {}
+
+   void pack(stencil::Direction dir, unsigned char* bufferPtr, IBlock* block, gpuStream_t stream) override
+   {
+      using Field_T    = typename Impl::Field_T;
+      using value_type = typename Field_T::value_type;
+      Field_T& field   = *block->getData< Field_T >(fieldId_);
+      CellInterval ci;
+      field->getSliceBeforeGhostLayer(dir, ci, sliceWidth_, false);
+      std::span< value_type > buffer{ static_cast< value_type* >(bufferPtr), this->size(dir, block) };
+      impl().doPack(field, buffer, dir, stream);
+   }
+
+   void unpack(stencil::Direction dir, unsigned char* buffer, IBlock* block, gpuStream_t stream) override
+   {
+      using Field_T    = typename Impl::Field_T;
+      using value_type = typename Field_T::value_type;
+      Field_T& field   = *block->getData< Field_T >(fieldId_);
+      CellInterval ci;
+      field->getGhostRegion(dir, ci, sliceWidth_, false);
+      std::span< value_type > buffer{ static_cast< value_type* >(bufferPtr), this->size(dir, block) };
+      impl().doUnpack(field, buffer, dir, stream);
+   }
+
+   uint_t size(stencil::Direction dir, IBlock* block) override
+   {
+      using Field_T = typename Impl::Field_T;
+
+      auto field = block->getData< Field_T >(fieldId_);
+      CellInterval ci;
+      field->getGhostRegion(dir, ci, 1, false);
+
+      uint_t elementsPerCell{ impl().elementsPerCell(dir) };
+      return elementsPerCell * ci.numCells();
+   }
+
+ protected:
+   BlockDataID fieldId_;
+   uint_t sliceWidth_;
+
+ private:
+   Impl& impl() { return static_cast< Impl& >(*this); }
+};
+
+} // namespace walberla::experimental::communication
+
+// #endif