diff --git a/src/walberla/codegen/communication/__init__.py b/src/walberla/codegen/communication/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..5ed6fdb7f3b34b538169f058cd4d9c38544cf3a5
--- /dev/null
+++ b/src/walberla/codegen/communication/__init__.py
@@ -0,0 +1,3 @@
+from .pack_infos import GpuFieldPackInfo
+
+__all__ = ["GpuFieldPackInfo"]
diff --git a/src/walberla/codegen/communication/pack_infos.py b/src/walberla/codegen/communication/pack_infos.py
new file mode 100644
index 0000000000000000000000000000000000000000..9989c9ca025fe52c9d78b741e30aac045cc310ca
--- /dev/null
+++ b/src/walberla/codegen/communication/pack_infos.py
@@ -0,0 +1,146 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+
+from pystencils import (
+    Field,
+    FieldType,
+    Assignment,
+    CreateKernelConfig,
+    DynamicType,
+    Target,
+)
+from pystencils.stencil import offset_to_direction_string
+from lbmpy import LBStencil
+
+from pystencilssfg import SfgComposer
+from pystencilssfg.composer.basic_composer import KernelsAdder
+from pystencilssfg.composer.custom import CustomGenerator
+from pystencilssfg.ir import SfgKernelHandle
+from pystencilssfg.lang.cpp import std
+from pystencilssfg.lang.gpu import CudaAPI, HipAPI
+
+from ..api import GpuFieldPtr, Direction, CellInterval
+from ..build_config import get_build_config
+
+
+@dataclass
+class PackingKernelsContext:
+    sfg: SfgComposer
+    kns: KernelsAdder
+    cfg: CreateKernelConfig
+
+
+class GpuFieldPackInfo(CustomGenerator):
+    def __init__(self, name: str, stencil: LBStencil, field: Field):
+        if field.index_dimensions > 1:
+            raise ValueError(
+                "GpuFieldPackInfo currently does not support higher-order tensor fields"
+            )
+
+        if isinstance(field.dtype, DynamicType):
+            raise ValueError(
+                "Cannot generate GpuFieldPackInfo for a dynamically-typed field"
+            )
+
+        self._name = name
+        self._stencil = stencil
+        self._field = field
+        self._dtype = field.dtype
+
+    def generate(self, sfg: SfgComposer) -> None:
+        base_class = f"walberla::experimental::communication::UniformGpuFieldPackInfoImpl< {self._name } >"
+
+        build_config = get_build_config(sfg)
+
+        pkc = PackingKernelsContext(
+            sfg,
+            kns=sfg.kernel_namespace(f"{self._name}_kernels"),
+            cfg=build_config.get_pystencils_config(),
+        )
+
+        # GpuAPI: type[ProvidesGpuRuntimeAPI]
+        match pkc.cfg.get_target():
+            case Target.CUDA:
+                GpuAPI = CudaAPI
+            case Target.HIP:
+                GpuAPI = HipAPI
+            case other:
+                raise ValueError(
+                    f"Invalid target for generating GpuFieldPackInfo: {other}"
+                )
+
+        pack_kernels: dict[tuple[int, int, int], SfgKernelHandle] = dict()
+        unpack_kernels: dict[tuple[int, int, int], SfgKernelHandle] = dict()
+
+        for comm_dir in self._stencil:
+            pack_kernels[comm_dir] = self._do_pack(pkc, comm_dir)
+            unpack_kernels[comm_dir] = self._do_unpack(pkc, comm_dir)
+
+        gpu_field = GpuFieldPtr.create(self._field)
+        buffer_span = std.span(self._dtype).var("buffer")
+        dir = Direction().var("dir")
+        ci = CellInterval().var("ci")
+        stream = GpuAPI.stream_t().var("stream")
+
+        common_buffer = self._buffer(1)
+
+        sfg.klass(self._name, bases=[f"public {base_class}"])(
+            sfg.public(
+                f"using Field_T = {gpu_field.get_dtype().c_string()};",
+                sfg.method("doPack").params(gpu_field, buffer_span, dir, ci, stream)(
+                    sfg.map_field(self._field, gpu_field),
+                    sfg.map_field(common_buffer, buffer_span),
+                    sfg.switch(dir).cases(
+                        {
+                            f"walberla::stencil::Direction::{offset_to_direction_string(comm_dir)}": sfg.gpu_invoke(
+                                pack_kernels[comm_dir], stream=stream
+                            )
+                            for comm_dir in self._stencil
+                        }
+                    ),
+                ),
+                sfg.method("doUnpack").params(gpu_field, buffer_span, dir, ci, stream)(
+                    sfg.map_field(self._field, gpu_field),
+                    sfg.map_field(common_buffer, buffer_span),
+                    sfg.switch(dir).cases(
+                        {
+                            f"walberla::stencil::Direction::{offset_to_direction_string(comm_dir)}": sfg.gpu_invoke(
+                                unpack_kernels[comm_dir], stream=stream
+                            )
+                            for comm_dir in self._stencil
+                        }
+                    ),
+                ),
+            )
+        )
+
+    def _pack_accesses(self, comm_dir: tuple[int, int, int]):
+        return [self._field.center(i) for i in range(self._field.index_shape[0])]
+
+    def _do_pack(
+        self, pkc: PackingKernelsContext, comm_dir: tuple[int, int, int]
+    ) -> SfgKernelHandle:
+        pack_accs = self._pack_accesses(comm_dir)
+        buffer = self._buffer(len(pack_accs))
+        asms = [Assignment(buffer(i), acc) for i, acc in enumerate(pack_accs)]
+        dir_str = offset_to_direction_string(comm_dir)
+        return pkc.kns.create(asms, f"pack{dir_str}", pkc.cfg)
+
+    def _do_unpack(
+        self, pkc: PackingKernelsContext, comm_dir: tuple[int, int, int]
+    ) -> SfgKernelHandle:
+        pack_accs = self._pack_accesses(comm_dir)
+        buffer = self._buffer(len(pack_accs))
+        asms = [Assignment(acc, buffer(i)) for i, acc in enumerate(pack_accs)]
+        dir_str = offset_to_direction_string(comm_dir)
+        return pkc.kns.create(asms, f"unpack{dir_str}", pkc.cfg)
+
+    def _buffer(self, num_elems: int):
+        return Field.create_generic(
+            "buffer",
+            1,
+            field_type=FieldType.BUFFER,
+            dtype=self._field.dtype,
+            index_shape=(num_elems,),
+        )
diff --git a/tests/BasicLbmScenarios/PackInfo.py b/tests/BasicLbmScenarios/PackInfo.py
new file mode 100644
index 0000000000000000000000000000000000000000..aa88327f7c538c06611260a8cc539cec553604a6
--- /dev/null
+++ b/tests/BasicLbmScenarios/PackInfo.py
@@ -0,0 +1,12 @@
+import pystencils as ps
+from lbmpy import Stencil, LBStencil
+from pystencilssfg import SourceFileGenerator
+from walberla.codegen.communication import GpuFieldPackInfo
+from walberla.codegen.build_config import DEBUG_MOCK_CMAKE
+
+DEBUG_MOCK_CMAKE.use_hip_default()
+
+with SourceFileGenerator() as sfg:
+    field = ps.fields("f(3): double[3D]")
+    stencil = LBStencil(Stencil.D3Q19)
+    sfg.generate(GpuFieldPackInfo("PackInfo", stencil, field))