Merge pull request #1790 from selvintxavier/wr_api_fixes

bnxt_re/lib: Fixes in WR APIs
diff --git a/buildlib/pyverbs_functions.cmake b/buildlib/pyverbs_functions.cmake
index 57aec04..868b75e 100644
--- a/buildlib/pyverbs_functions.cmake
+++ b/buildlib/pyverbs_functions.cmake
@@ -61,6 +61,13 @@
 endfunction()
 
 function(rdma_python_module PY_MODULE)
+  if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/__version__.py.in")
+    message(FATAL_ERROR "Can't generate python module ${PY_MODULE} version, need ${CMAKE_CURRENT_SOURCE_DIR}/__version__.py.in")
+  endif()
+  configure_file("${CMAKE_CURRENT_SOURCE_DIR}/__version__.py.in"
+    "${BUILD_PYTHON}/${PY_MODULE}/__version__.py" @ONLY)
+  install(FILES "${BUILD_PYTHON}/${PY_MODULE}/__version__.py"
+    DESTINATION ${CMAKE_INSTALL_PYTHON_ARCH_LIB}/${PY_MODULE})
   foreach(PY_FILE ${ARGN})
     get_filename_component(LINK "${CMAKE_CURRENT_SOURCE_DIR}/${PY_FILE}" ABSOLUTE)
     rdma_create_symlink("${LINK}" "${BUILD_PYTHON}/${PY_MODULE}/${PY_FILE}")
diff --git a/debian/ibverbs-providers.symbols b/debian/ibverbs-providers.symbols
index 40e627d..044b4c9 100644
--- a/debian/ibverbs-providers.symbols
+++ b/debian/ibverbs-providers.symbols
@@ -211,8 +211,10 @@
 libmana.so.1 ibverbs-providers #MINVER#
 * Build-Depends-Package: libibverbs-dev
  MANA_1.0@MANA_1.0 41
+ MANA_1.1@MANA_1.1 65
  manadv_init_obj@MANA_1.0 41
  manadv_set_context_attr@MANA_1.0 41
+ manadv_alloc_pd@MANA_1.1 65
 libionic.so.1 ibverbs-providers #MINVER#
 * Build-Depends-Package: libibverbs-dev
  IONIC_1.0@IONIC_1.0 59
diff --git a/kernel-boot/rdma_topo b/kernel-boot/rdma_topo
index aa43faf..9b280a8 100755
--- a/kernel-boot/rdma_topo
+++ b/kernel-boot/rdma_topo
@@ -15,16 +15,17 @@
 import subprocess
 import sys
 import tempfile
+import textwrap
 
 from abc import ABC, abstractmethod
 from base64 import b64encode, b64decode
+from enum import Enum
 from typing import *
 from zlib import compress, decompress
 
 DEVDIR = os.environ.get("RDMA_TOPO_DEVDIR", "/sys/bus/pci/devices/")
 
 BDF_RE = re.compile(r"^([0-9a-f]+?):([0-9a-f]{2}?):([0-9a-f]{2}?)\.([0-9a-f])$")
-KERNEL_ACS_ISOLATED = "xx111x1"
 pci_vendors = {
     "MELLANOX": 0x15B3,
     "NVIDIA": 0x10DE,
@@ -41,10 +42,43 @@
 
 
 class CommandError(Exception):
-    pass
+    class Level(Enum):
+        ERROR = "E"
+        WARNING = "W"
+
+    def __init__(self, reason: str, level: Level = Level.ERROR):
+        self.reason = reason
+        self.level = level
+        super().__init__(f"{level.value}: {reason}")
+
+    def __str__(self):
+        return f"{self.level.value}: {self.reason}"
 
 
-TOPO_NOT_SUPPORTED = CommandError("No supported topology detected")
+class TopoNotSupportedError(CommandError):
+    def __init__(self, reason: str = ""):
+        details = ""
+        if len(reason) > 0:
+            details = f": {reason}"
+        super().__init__(f"No supported topology detected{details}",
+                         CommandError.Level.ERROR)
+
+
+class TopoUnexpectedError(CommandError):
+    def __init__(self, reason: str = ""):
+        details = ""
+        if len(reason) > 0:
+            details = f": {reason}"
+        super().__init__(
+            (f"Unexpected topology structure{details}. Are you running on a "
+             "production system? If yes, please report this issue."),
+            CommandError.Level.ERROR,
+        )
+
+
+class DumpParsingError(CommandError):
+    def __init__(self, reason: str):
+        super().__init__(f"Malformed dump: {reason}", CommandError.Level.ERROR)
 
 
 def yesno(b: bool) -> str:
@@ -146,7 +180,7 @@
                 try:
                     obj.data[k] = decompress(b64decode(obj.data[k]))
                 except Exception as e:
-                    raise ValueError(f"Invalid encoded value for key '{k}': {e}")
+                    raise ValueError(f"Invalid encoded value for key '{k}': {e}") from e
 
         return obj
 
@@ -162,13 +196,16 @@
         return res
 
 
-def parse_vpd(vpd: Optional[bytes]) -> Tuple[Optional[str], Optional[str]]:
-    """Parse VPD name and V3 UUID"""
+def parse_vpd(
+    vpd: Optional[bytes],
+) -> Tuple[Optional[str], Optional[str], Optional[str]]:
+    """Parse VPD name, V3 UUID, and serial number (SN)"""
     if vpd is None:
-        return None, None
+        return None, None, None
 
     name = None
     v3 = None
+    sn = None
 
     def items(data: bytes) -> Generator[Tuple[int, bytes]]:
         while len(data) > 0:
@@ -207,10 +244,12 @@
                 for keyword, value in keywords(item):
                     if keyword == "V3":
                         v3 = value.decode("ascii")
+                    elif keyword == "SN":
+                        sn = value.decode("ascii")
     except UnicodeDecodeError:
         pass
 
-    return (v3, name)
+    return (v3, name, sn)
 
 
 def parse_ext_cap(config: bytes, cap_id: int) -> Optional[bytes]:
@@ -248,6 +287,13 @@
     return parse_ext_cap(config, PCI_EXT_CAP_ID_ATS) is not None
 
 
+def kernel_acs_isolated(device_type: str) -> str:
+    if device_type == "vera_rp":
+        # Advertises P2P Completion Redirect (bit-3) as read-only and disabled.
+        return "xx101x1"
+    return "xx111x1"
+
+
 def PCI_VDEVICE(vendor: str, device_id: int) -> re.Pattern:
     """Match a Vendor and device ID"""
     vendor_id = pci_vendors[vendor]
@@ -275,8 +321,11 @@
     PCI_VDEVICE("NVIDIA", 0x22B1): "grace_rp",  # NVIDIA Grace PCI Root Port Bridge
     PCI_VDEVICE("NVIDIA", 0x22B2): "grace_rp",  # NVIDIA Grace PCI Root Port Bridge
     PCI_VDEVICE("NVIDIA", 0x22B8): "grace_rp",  # NVIDIA Grace PCI Root Port Bridge
+    PCI_VDEVICE("NVIDIA", 0x2F95): "vera_rp",  # NVIDIA Vera PCIe Root Port Bridge
+    PCI_VDEVICE("NVIDIA", 0x2F96): "vera_rp",  # NVIDIA Vera PCIe Root Port Bridge
     PCI_VDEVICE("MELLANOX", 0x1021): "cx_nic",  # ConnectX-7
     PCI_VDEVICE("MELLANOX", 0x1023): "cx_nic",  # ConnectX-8
+    PCI_VDEVICE("MELLANOX", 0x1025): "cx_nic",  # ConnectX-9
     PCI_VDEVICE("MELLANOX", 0xA2DC): "bf3_nic",  # BlueField-3
     PCI_VDEVICE("MELLANOX", 0x2100): "cx_dma",  # ConnectX-8 DMA Controller
     PCI_VDEVICE("MELLANOX", 0x197B): "bf3_switch",  # USP/DSP of a BF3 switch
@@ -324,6 +373,7 @@
     device_type = ""
     vpd_v3: Optional[str] = None
     vpd_name: Optional[str] = None
+    vpd_sn: Optional[str] = None
     parent: PCIDevice = None
 
     def __init__(self, bdf: PCIBDF, sysfs_device: SysfsDevice):
@@ -350,7 +400,7 @@
     def finish_loading(self):
         """Do more expensive parsing operations"""
         if self.device_type == "cx_nic" or self.device_type == "cx_dma":
-            self.vpd_v3, self.vpd_name = parse_vpd(self.sysfs_device.vpd)
+            self.vpd_v3, self.vpd_name, self.vpd_sn = parse_vpd(self.sysfs_device.vpd)
         if "switch" in self.device_type or self.device_type.endswith("_rp"):
             self.has_acs = self.get_acs_ctrl() is not None
         if self.device_type == "cx_nic":
@@ -387,6 +437,135 @@
         return self.sysfs_device.subsystems or {}
 
 
+class NVCX_Topo:
+
+    class NIC(object):
+        def __init__(self, pfs: Set[PCIDevice]):
+            assert len(pfs) > 0
+            self.pfs = pfs
+            self.parent = min(pfs, key=lambda pf: pf.bdf).parent
+            self.vpd_sn = min(pfs, key=lambda pf: pf.bdf).vpd_sn
+            if not all(pf.parent == self.parent for pf in pfs):
+                raise TopoUnexpectedError("All PFs of a NIC must have the same parent")
+            if not all(pf.vpd_sn == self.vpd_sn for pf in pfs):
+                raise TopoUnexpectedError("All PFs of a NIC must have the same VPD SN")
+
+        @property
+        def primary_pf(self) -> PCIDevice:
+            return min(self.pfs, key=lambda pf: pf.bdf)
+
+        def to_dict(self) -> Dict[str, Any]:
+            return {
+                "parent_bdf": str(self.parent.bdf) if self.parent else "UNKNOWN",
+                "ats": self.primary_pf.has_ats,
+                "pf_bdfs": [
+                    str(pf.bdf) for pf in sorted(self.pfs, key=lambda pf: pf.bdf)
+                ],
+            }
+
+        def str_single_pf(self) -> str:
+            res = ""
+            res += f"\tNIC ATS: {yesno(self.primary_pf.has_ats)}\n"
+            subsystems: Dict[str, Set[str]] = collections.defaultdict(set)
+            for pf in self.pfs:
+                for k, v in pf.get_subsystems().items():
+                    subsystems[k].update(v)
+            res += print_list("RDMA device", subsystems["infiniband"])
+            res += print_list("Net device", subsystems["net"])
+            return res[:-1]
+
+        def __str__(self) -> str:
+            parent_bdf = self.parent.bdf if self.parent else "UNKNOWN"
+            res = f"RDMA NIC Parent={parent_bdf}\n"
+            res += print_list("NIC PCI device", [str(pf.bdf) for pf in self.pfs])
+            res += self.str_single_pf()
+            return res
+
+    class Board(object):
+        def __init__(self, nics: Set[NVCX_Topo.NIC]):
+            def nic_sort_key(nic):
+                if nic.parent:
+                    return (0, nic.parent.bdf)
+                return (1, nic.primary_pf.bdf)
+
+            self.nics = sorted(nics, key=nic_sort_key)
+            self.sn = next(iter(nics)).vpd_sn
+            if not all(nic.vpd_sn == self.sn for nic in nics):
+                raise TopoUnexpectedError("All NICs of a Board must have the same VPD SN")
+
+        def to_dict(self) -> Dict[str, Any]:
+            return {
+                "board_sn": self.sn or "UNKNOWN",
+                "nics": [nic.to_dict() for nic in self.nics],
+            }
+
+        def __str__(self) -> str:
+            board_sn = self.sn or "UNKNOWN"
+            res = f"RDMA NIC Board={board_sn}\n"
+            for nic in self.nics:
+                res += textwrap.indent(str(nic), "\t")
+                res += "\n"
+            return res[:-1]
+
+    def __init__(self, pfs: Set[PCIDevice]):
+        assert len(pfs) > 0
+
+        pfs_by_sn: Dict[Optional[str], Set[PCIDevice]] = collections.defaultdict(set)
+        for pf in pfs:
+            pfs_by_sn[pf.vpd_sn].add(pf)
+
+        boards = set()
+        for board_pfs in pfs_by_sn.values():
+            pfs_by_nic: Dict[Optional[PCIDevice], Set[PCIDevice]] = (
+                collections.defaultdict(set)
+            )
+            for pf in board_pfs:
+                pfs_by_nic[pf.parent].add(pf)
+
+            nics: Set[NVCX_Topo.NIC] = set()
+            for nic_pfs in pfs_by_nic.values():
+                nics.add(NVCX_Topo.NIC(nic_pfs))
+
+            boards.add(NVCX_Topo.Board(nics))
+
+        self.boards = sorted(boards, key=lambda b: b.sn or "")
+
+    @property
+    def primary_pf(self) -> PCIDevice:
+        return min(self.pfs, key=lambda pf: pf.bdf)
+
+    @property
+    def pfs(self) -> Set[PCIDevice]:
+        return set(pf for board in self.boards for nic in board.nics for pf in nic.pfs)
+
+    def to_dict(self) -> Dict[str, Any]:
+        if len(self.pfs) == 1:
+            return {
+                "rdma_nic_pf_bdf": str(next(iter(self.pfs)).bdf),
+                "rdma_nic_ats": next(iter(self.pfs)).has_ats,
+            }
+
+        return {
+            "rdma_nic_boards": [board.to_dict() for board in self.boards]
+        }
+
+    def topo_str_key(self) -> str:
+        if len(self.pfs) == 1:
+            return f"RDMA NIC={next(iter(self.pfs)).bdf}"
+
+        return ""
+
+    def topo_str(self) -> str:
+        if len(self.pfs) == 1:
+            return self.boards[0].nics[0].str_single_pf()
+
+        res = ""
+        for board in self.boards:
+            res += textwrap.indent(str(board), "\t")
+            res += "\n"
+        return res[:-1]
+
+
 class NVCX_Complex(ABC):
     @property
     @abstractmethod
@@ -598,23 +777,23 @@
             for pdev in dsp.iterdownstream():
                 if pdev.device_type == "cx_nic":
                     if self.cx_pf_dsp is not None:
-                        raise ValueError(
-                            f"Multiple CX NIC DSPs under the same shared switch not supported"
+                        raise TopoNotSupportedError(
+                            f"Multiple CX NIC DSPs under the same shared switch"
                         )
                     self.cx_pf_dsp = dsp
                     break
                 if pdev.device_type == "nvgpu":
                     if self.nvgpu_dsp is not None:
-                        raise ValueError(
-                            f"Multiple GPU DSPs under the same shared switch not supported"
+                        raise TopoNotSupportedError(
+                            f"Multiple GPU DSPs under the same shared switch"
                         )
                     self.nvgpu_dsp = dsp
                     break
 
         if not self.cx_pf_dsp:
-            raise ValueError(f"CX NIC DSP not found in the topology")
+            raise TopoUnexpectedError(f"CX NIC DSP not found in the topology")
         if not self.nvgpu_dsp:
-            raise ValueError(f"GPU DSP not found in the topology")
+            raise TopoUnexpectedError(f"GPU DSP not found in the topology")
 
     @property
     def primary_nic(self) -> PCIDevice:
@@ -622,13 +801,13 @@
 
     def compute_acs(self, virt: Optional[bool]) -> Dict[PCIDevice, str]:
         if not self.cx_pf_dsp.has_acs:
-            raise CommandError(f"CX NIC DSP {self.cx_pf_dsp.bdf} lacks ACS")
+            raise TopoUnexpectedError(f"CX NIC DSP {self.cx_pf_dsp.bdf} lacks ACS")
         if not self.nvgpu_dsp.has_acs:
-            raise CommandError(f"GPU DSP {self.nvgpu_dsp.bdf} lacks ACS")
+            raise TopoUnexpectedError(f"GPU DSP {self.nvgpu_dsp.bdf} lacks ACS")
         if not self.root_port.has_acs:
-            raise CommandError(f"Root port {self.root_port.bdf} lacks ACS")
+            raise TopoUnexpectedError(f"Root port {self.root_port.bdf} lacks ACS")
         if virt is None:
-            raise CommandError("Unexpected: Could not determine virt mode")
+            raise TopoUnexpectedError("Could not determine virt mode")
 
         if virt:
             return {
@@ -646,8 +825,8 @@
                 # bit-3 : ACS P2P Completion Redirect
                 # bit-2 : ACS P2P Request Redirect
                 # bit-0 : ACS Source Validation
-                self.nvgpu_dsp: KERNEL_ACS_ISOLATED,
-                self.root_port: KERNEL_ACS_ISOLATED,
+                self.nvgpu_dsp: kernel_acs_isolated(self.nvgpu_dsp.device_type),
+                self.root_port: kernel_acs_isolated(self.root_port.device_type),
             }
         else:
             return {
@@ -673,8 +852,7 @@
             res["rdma_nic_vpd_name"] = self.cx_pf.vpd_name
         if self.cx_pf.numa_node is not None:
             res["numa_node"] = self.cx_pf.numa_node
-        if self.cx_pf.has_ats:
-            res["rdma_nic_ats"] = self.cx_pf.has_ats
+        res["rdma_nic_ats"] = self.cx_pf.has_ats
 
         for pdev in sorted(
             itertools.chain([self.cx_pf, self.nvgpu]),
@@ -761,6 +939,113 @@
         return res[:-1]
 
 
+class NVCX_NUMA_Complex(NVCX_Complex):
+    def __init__(
+        self,
+        numa_node: int,
+        cx_pfs: Set[PCIDevice],
+        nvgpus: Set[PCIDevice],
+    ):
+        if not cx_pfs:
+            raise TopoUnexpectedError("No CX NICs found in NUMA-based complex")
+        if not nvgpus:
+            raise TopoUnexpectedError("No GPUs found in NUMA-based complex")
+        self.numa_node = numa_node
+        self.nvgpus = nvgpus
+        self.cx_topo = NVCX_Topo(cx_pfs)
+
+    @property
+    def primary_nic(self) -> PCIDevice:
+        return self.cx_topo.primary_pf
+
+    def compute_acs(self, _: Optional[bool]) -> Dict[PCIDevice, str]:
+        # All are connected to RPs, which are handled by PCITopo.compute_acs
+        return {}
+
+    def to_dict(self) -> Dict[str, Any]:
+        res = {
+            "numa_node": self.numa_node,
+            "gpu_bdfs": [str(I.bdf) for I in self.nvgpus],
+            "subsystems": {},
+        }
+        res.update(self.cx_topo.to_dict())
+        devname = self.cx_topo.primary_pf.vpd_name
+        if devname:
+            res["rdma_nic_vpd_name"] = devname
+        for pdev in sorted(
+            itertools.chain(self.cx_topo.pfs, self.nvgpus),
+            key=lambda x: x.bdf,
+        ):
+            subsystems = pdev.get_subsystems()
+            if subsystems:
+                res["subsystems"][str(pdev.bdf)] = {
+                    subsys: list(devs) for subsys, devs in subsystems.items()
+                }
+        return res
+
+    def __check_iommu_group(self) -> bool:
+        devs = [*self.cx_topo.pfs, *self.nvgpus]
+        iommu_groups = [dev.iommu_group for dev in devs]
+        devs_without_iommu_group = [dev for dev in devs if dev.iommu_group is None]
+        if len(devs_without_iommu_group) > 0:
+            bdfs = ", ".join([str(dev.bdf) for dev in devs_without_iommu_group])
+            check_fail(
+                f"Kernel iommu_group missing for devices on NUMA node {self.numa_node}: {bdfs}"
+            )
+            return False
+        if len(iommu_groups) == len(set(iommu_groups)):
+            check_ok(
+                f"All kernel iommu_groups for NUMA node {self.numa_node} are unique"
+            )
+            return True
+
+        duplicate_groups = [
+            group for group in iommu_groups if iommu_groups.count(group) > 1
+        ]
+        check_fail(
+            (f"Multiple devices share the same kernel iommu_group for "
+             f"NUMA node {self.numa_node}: {duplicate_groups}")
+        )
+        return False
+
+    def __check_ats(self) -> bool:
+        result = True
+        for cx_pf in self.cx_topo.pfs:
+            if cx_pf.has_ats:
+                check_fail(
+                    f"ATS capability for {cx_pf.device_type} {cx_pf.bdf} is available"
+                )
+                result = False
+            else:
+                check_ok(
+                    f"ATS capability for {cx_pf.device_type} {cx_pf.bdf} is not available"
+                )
+        return result
+
+    def check(self, virt: Optional[bool]) -> bool:
+        res_ats = self.__check_ats()
+        res_iommu_group = self.__check_iommu_group()
+        return res_ats and res_iommu_group
+
+    def __str__(self) -> str:
+        res = f"NUMA Node={self.numa_node}\n"
+        devname = self.primary_nic.vpd_name
+        if devname:
+            res += f"\t{devname}\n"
+        res += f"{self.cx_topo.topo_str()}\n"
+        res += print_list("GPU PCI device", [str(I.bdf) for I in self.nvgpus])
+
+        gpu_subsystems: Set[str] = set()
+        for pdev in self.nvgpus:
+            subsys = pdev.get_subsystems()
+            if "drm" in subsys:
+                gpu_subsystems.update(subsys["drm"])
+        if len(gpu_subsystems) > 0:
+            res += print_list("DRM device", gpu_subsystems)
+
+        return res[:-1]
+
+
 def check_parent(pdev: PCIDevice, parent_type: str):
     if not pdev or not pdev.parent:
         return None
@@ -771,6 +1056,10 @@
 
 class PCITopo(object):
     """Load the PCI topology from sysfs and organize it"""
+    class TopoType(Enum):
+        INLINE = "Inline"
+        DMA = "DMA-based"
+        NUMA = "NUMA-based"
 
     def __init__(
         self,
@@ -784,37 +1073,45 @@
             sysfs_devices = [SysfsDevice(fn) for fn in os.listdir(DEVDIR)]
         self.devices = self.__load_devices(sysfs_devices)
         self.nvcxs: List[NVCX_Complex] = []
-        self.has_cx_dma = any(
-            pdev.device_type == "cx_dma" for pdev in self.devices.values()
-        )
-        self.has_gpu_and_nic = False
+        self.type = self.__detect_topo_type()
 
-        if self.has_cx_dma and virt is not None:
+        if self.type == self.TopoType.DMA and virt is not None:
             raise CommandError(
-                "--virt / --no-virt is not supported on DMA-based topologies"
+                f"--virt / --no-virt is not supported on {self.type.value} topologies"
             )
         self.virt = virt
         self._autodetect_virt = autodetect_virt
 
-        if not self.has_cx_dma:
-            found = {
-                "cx_switch": False,
-                "nvgpu": False,
-                "cx_nic": False,
-            }
-            for pdev in self.devices.values():
-                if pdev.device_type not in found.keys():
-                    continue
-                found[pdev.device_type] = True
-            self.has_gpu_and_nic = all(found.values())
-
-            if not self.has_gpu_and_nic:
-                return
-
         for pdev in self.devices.values():
             pdev.finish_loading()
         self.__build_topo()
 
+    def __detect_topo_type(self) -> TopoType:
+        if any(pdev.device_type == "cx_dma" for pdev in self.devices.values()):
+            return self.TopoType.DMA
+
+        if any(
+            pdev.device_type == "nvgpu"
+            and pdev.parent is not None
+            and pdev.parent.device_type.endswith("_rp")
+            for pdev in self.devices.values()
+        ):
+            return self.TopoType.NUMA
+
+        found = {
+            "cx_switch": False,
+            "nvgpu": False,
+            "cx_nic": False,
+        }
+        for pdev in self.devices.values():
+            if pdev.device_type not in found.keys():
+                continue
+            found[pdev.device_type] = True
+        if all(found.values()):
+            return self.TopoType.INLINE
+
+        raise TopoNotSupportedError()
+
     def __parse_dump(self, filename: str) -> List[SysfsDevice]:
         res: List[SysfsDevice] = []
         try:
@@ -836,7 +1133,7 @@
                     raise ValueError(f"Item {i}/{num_items}: {e}") from e
             return res
         except (json.JSONDecodeError, ValueError) as e:
-            raise CommandError(f"Invalid sysfs dump file: {e}")
+            raise DumpParsingError(str(e))
         except (FileNotFoundError, PermissionError) as e:
             raise CommandError(f"Failed to read sysfs dump file: {e}")
 
@@ -848,6 +1145,10 @@
                 continue
             assert bdf not in res
             res[bdf] = PCIDevice(bdf, sdev)
+        for pdev in res.values():
+            if pdev.parent_bdf and pdev.parent_bdf in res:
+                pdev.parent = res[pdev.parent_bdf]
+                pdev.parent.children.add(pdev)
         return res
 
     def __get_nvcx_complex(self, cx_dma: PCIDevice):
@@ -859,22 +1160,21 @@
         """
         assert cx_dma.device_type == "cx_dma"
         if not cx_dma.vpd_v3:
-            raise ValueError(f"CX DMA function {cx_dma} does not have a VPD V3 UUID")
+            raise TopoUnexpectedError(f"CX DMA function {cx_dma} does not have a VPD V3 UUID")
 
         # The DMA and PF are matched using the UUID from the VPD
         cx_pfs = self.vpd_v3s.get(cx_dma.vpd_v3)
         if cx_pfs is None:
-            raise ValueError(
+            raise TopoUnexpectedError(
                 f"CX DMA function {cx_dma} does not have a matching PF, V3 UUID matching failed"
             )
-            return None
 
         # Path from the DMA to the root port
         cx_dma_dsp = check_parent(cx_dma, "cx_switch")
         cx_usp = check_parent(cx_dma_dsp, "cx_switch")
         grace_rp = check_parent(cx_usp, "grace_rp")
         if not grace_rp:
-            raise ValueError(
+            raise TopoUnexpectedError(
                 f"CX DMA function {cx_dma} has an unrecognized upstream path"
             )
 
@@ -883,13 +1183,13 @@
             pdev for pdev in grace_rp.iterdownstream() if pdev.device_type == "nvgpu"
         ]
         if len(nvgpus) != 1:
-            raise ValueError(f"CX DMA function {cx_dma} does not have a nearby GPU")
+            raise TopoUnexpectedError(f"CX DMA function {cx_dma} does not have a nearby GPU")
         nvgpu = nvgpus[0]
         nvgpu_dsp2 = check_parent(nvgpu, "cx_switch")
         nvgpu_usp2 = check_parent(nvgpu_dsp2, "cx_switch")
         nvgpu_dsp1 = check_parent(nvgpu_usp2, "cx_switch")
         if cx_usp != check_parent(nvgpu_dsp1, "cx_switch"):
-            raise ValueError(
+            raise TopoNotSupportedError(
                 f"CX DMA function {cx_dma} has an unrecognized upstream path from the GPU"
             )
 
@@ -905,7 +1205,7 @@
         }
         topodevs = set(grace_rp.iterdownstream())
         if alldevs != topodevs:
-            raise ValueError(
+            raise TopoUnexpectedError(
                 f"CX DMA function {cx_dma} has unexpected PCI devices in the topology"
             )
 
@@ -924,14 +1224,16 @@
         nvgpu_dsp1 = check_parent(nvgpu_usp2, "cx_switch")
         shared_usp1 = check_parent(nvgpu_dsp1, "cx_switch")
         if not shared_usp1:
-            raise ValueError(f"GPU {nvgpu} has an unrecognized upstream path")
+            raise TopoNotSupportedError(
+                f"GPU {nvgpu.bdf} has an unrecognized upstream path"
+            )
 
         for pdev in shared_usp1.iterupstream_path():
             if pdev.device_type == "generic_rp":
                 root_port = pdev
                 break
         else:
-            raise ValueError(
+            raise TopoUnexpectedError(
                 f"Could not find root port for shared USP {shared_usp1.bdf}"
             )
 
@@ -940,16 +1242,34 @@
                 cx_nic = pdev
                 break
         else:
-            raise ValueError(f"GPU {nvgpu} does not have a nearby CX NIC")
+            raise TopoUnexpectedError(f"GPU {nvgpu} does not have a nearby CX NIC")
 
         return NVCX_Inline_Complex(root_port, shared_usp1, cx_nic, nvgpu)
 
+    def __get_nvcx_numa_complex(self, numa_node: int):
+        """Match the topology for the NUMA complex.
+
+        All CX NICs and GPUs on the same NUMA node are part of the same complex.
+        """
+        cx_pfs = [
+            pdev
+            for pdev in self.devices.values()
+            if pdev.numa_node == numa_node and pdev.device_type == "cx_nic"
+        ]
+        nvgpus = [
+            pdev
+            for pdev in self.devices.values()
+            if pdev.numa_node == numa_node and pdev.device_type == "nvgpu"
+        ]
+        return NVCX_NUMA_Complex(numa_node, cx_pfs, nvgpus)
+
     def __auto_detect_virt(self) -> bool:
         """Auto-detect if virtualization will be used on this system"""
         first = self.nvcxs[0].primary_nic.has_ats
         if not all(nvcx.primary_nic.has_ats == first for nvcx in self.nvcxs):
-            raise CommandError(
-                "Could not auto-detect virtualization: CX NICs have different ATS settings"
+            raise TopoNotSupportedError(
+                "Could not auto-detect virtualization. CX NICs have different"
+                " ATS settings. Try explicitly setting --virt or --no-virt."
             )
 
         return first
@@ -959,36 +1279,40 @@
         objects for the cx_dma functions"""
         self.vpd_v3s: Dict[str, Set[PCIDevice]] = collections.defaultdict(set)
         for pdev in self.devices.values():
-            if pdev.parent_bdf and pdev.parent_bdf in self.devices:
-                pdev.parent = self.devices[pdev.parent_bdf]
-                pdev.parent.children.add(pdev)
-
             # Many PCI functions may share the same V3
             if pdev.vpd_v3:
                 self.vpd_v3s[pdev.vpd_v3].add(pdev)
 
-        if self.has_cx_dma:
+        if self.type == self.TopoType.DMA:
             for pdev in self.devices.values():
                 if pdev.device_type == "cx_dma":
                     nvcx = self.__get_nvcx_complex(pdev)
                     self.nvcxs.append(nvcx)
-        elif self.has_gpu_and_nic:
+        elif self.type == self.TopoType.INLINE:
             for pdev in self.devices.values():
                 if pdev.device_type == "nvgpu":
                     nvcx = self.__get_nvcx_inline_complex(pdev)
                     self.nvcxs.append(nvcx)
+        elif self.type == self.TopoType.NUMA:
+            numa_nodes = {
+                pdev.numa_node
+                for pdev in self.devices.values()
+                if pdev.numa_node is not None and pdev.numa_node >= 0
+            }
+            for numa in numa_nodes:
+                self.nvcxs.append(self.__get_nvcx_numa_complex(numa))
 
-        if self.has_gpu_and_nic and len(self.nvcxs) > 0:
+        if len(self.nvcxs) == 0:
+            raise TopoNotSupportedError(
+                f"No supported complex found for {self.type.value} topology"
+            )
+
+        if self.type == self.TopoType.INLINE:
             if self.virt is None and self._autodetect_virt:
                 self.virt = self.__auto_detect_virt()
 
         self.nvcxs.sort(key=lambda x: x.primary_nic.bdf)
 
-    @property
-    def supported(self) -> bool:
-        """True if the system has a topology that is supported by the rdma_topo tool"""
-        return (self.has_cx_dma or self.has_gpu_and_nic) and len(self.nvcxs) > 0
-
     def compute_acs(self):
         """Return a dictionary of PCI devices and the ACS mask the device should
         have"""
@@ -997,8 +1321,8 @@
             acs.update(nvcx.compute_acs(self.virt))
 
         # Enable, using kernel default, or disable ACS on all other CX
-        # bridges and Grace RP based on the virt parameter or if the topology
-        # has CX DMA functions.
+        # bridges and Grace, Vera RPs based on the virt parameter or if the
+        # topology is DMA-based or NUMA-based.
         #
         # To enable (matches kernel default):
         # bit-4 : ACS Upstream Forwarding
@@ -1008,11 +1332,16 @@
         for pdev in self.devices.values():
             if (
                 pdev not in acs
-                and ("switch" in pdev.device_type or "grace_rp" in pdev.device_type)
+                and (
+                    "switch" in pdev.device_type
+                    or pdev.device_type in ["grace_rp", "vera_rp"]
+                )
                 and pdev.has_acs
             ):
                 acs[pdev] = (
-                    KERNEL_ACS_ISOLATED if self.has_cx_dma or self.virt else "xx000x0"
+                    kernel_acs_isolated(pdev.device_type)
+                    if self.type in [self.TopoType.DMA, self.TopoType.NUMA] or self.virt
+                    else "xx000x0"
                 )
         return acs
 
@@ -1072,8 +1401,6 @@
     """List the ConnectX NICs in the system with the corresponding NIC
     function, associated GPU, and, optionally, DMA Direct function."""
     topo = PCITopo(args.sysfs_dump, virt=None, autodetect_virt=False)
-    if not topo.supported:
-        raise TOPO_NOT_SUPPORTED
 
     if args.json:
         return topo_json(topo)
@@ -1131,22 +1458,25 @@
     If the system does not have any need of ACS flags the dropin file will be
     removed. This command is intended for Debian style systems with a
     /etc/default/grub.d and update-grub command."""
-    topo = PCITopo(None, args.virt)
-    if not topo.supported:
+    try:
+        topo = PCITopo(None, args.virt)
+    except TopoNotSupportedError as e:
         if args.dry_run:
-            raise TOPO_NOT_SUPPORTED
+            raise
         if os.path.exists(args.output):
-            print(
-                f"W: Found ACS drop-in file {args.output} but the system does not have a supported topology. Deleting file."
-            )
             os.unlink(args.output)
+            raise CommandError(
+                (f"Found ACS drop-in file {args.output} but the system does "
+                 "not have a supported topology. File deleted."),
+                CommandError.Level.WARNING,
+            )
         return
 
     acs = topo.compute_acs()
     config_acs = [
         f"{acs}@{pdev.bdf}"
         for pdev, acs in sorted(acs.items(), key=lambda x: x[0].bdf)
-        if acs != KERNEL_ACS_ISOLATED
+        if acs != kernel_acs_isolated(pdev.device_type)
     ]
     acs_arg = ";".join(config_acs)
     grub_conf = [
@@ -1199,8 +1529,6 @@
     failures, use with caution!
     """
     topo = PCITopo(None, args.virt)
-    if not topo.supported:
-        raise TOPO_NOT_SUPPORTED
     acs = topo.compute_acs()
     cmds: List[List[str]] = []
     for pdev, acs in sorted(acs.items(), key=lambda x: x[0].bdf):
@@ -1241,12 +1569,12 @@
     """Check that the running kernel and PCI environment are setup correctly for
     GPU Direct with ConnectX DMA Direct PCI functions."""
     topo = PCITopo(args.sysfs_dump, args.virt)
-    if not topo.supported:
-        raise TOPO_NOT_SUPPORTED
-    if topo.has_cx_dma:
+    if topo.type == PCITopo.TopoType.DMA:
         check_ok("All ConnectX DMA functions have correct PCI topology")
-    elif topo.has_gpu_and_nic:
+    elif topo.type == PCITopo.TopoType.INLINE:
         check_ok("All NIC/GPU complexes have correct PCI topology")
+    elif topo.type == PCITopo.TopoType.NUMA:
+        check_ok("All NUMA-based complexes have correct PCI topology")
 
     fatal = False
     acs = topo.compute_acs()
@@ -1351,8 +1679,8 @@
     try:
         args.func(args)
     except CommandError as e:
-        print(f"E: {e}")
-        sys.exit(100)
+        print(e)
+        sys.exit(100 if e.level == CommandError.Level.ERROR else 0)
 
 
 main()
diff --git a/kernel-boot/rdma_topo_unit_test.py b/kernel-boot/rdma_topo_unit_test.py
new file mode 100644
index 0000000..3482023
--- /dev/null
+++ b/kernel-boot/rdma_topo_unit_test.py
@@ -0,0 +1,611 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: Linux-OpenIB
+# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES
+"""Unit tests for NVCX_Topo and its nested NIC / Board types.
+
+Tests import rdma_topo via load_rdma_topo() (which strips the bare main()
+call) and use lightweight mock PCIDevice objects so no sysfs or dump file
+is needed.
+"""
+from __future__ import annotations
+
+import sys
+import types
+
+from pathlib import Path
+from typing import Dict, List, Optional
+from unittest.mock import MagicMock
+
+try:
+    import pytest
+except ImportError:
+    print("Missing dependency: pytest", file=sys.stderr)
+    print("Install with: pip3 install pytest", file=sys.stderr)
+    sys.exit(1)
+
+HERE = Path(__file__).resolve().parent
+RDMA_TOPO = HERE / "rdma_topo"
+
+
+def _strip_trailing_main_call(src: str) -> str:
+    """rdma_topo ends with bare main(); skip it so test import does not run CLI."""
+    lines = src.splitlines()
+    i = len(lines) - 1
+    while i >= 0 and lines[i].strip() == "":
+        i -= 1
+    if i < 0:
+        return src
+    if lines[i].split("#", 1)[0].strip() == "main()":
+        return "\n".join(lines[:i]) + ("\n" if i else "")
+    return src
+
+
+def load_rdma_topo():
+    raw = RDMA_TOPO.read_text(encoding="utf-8")
+    code_s = _strip_trailing_main_call(raw)
+    mod = types.ModuleType("rdma_topo")
+    mod.__file__ = str(RDMA_TOPO)
+    mod.__name__ = "rdma_topo"
+    mod.__package__ = ""
+    sys.modules["rdma_topo"] = mod
+    exec(compile(code_s, str(RDMA_TOPO), "exec"), mod.__dict__)
+    return mod
+
+
+load_rdma_topo()
+
+from rdma_topo import PCIBDF, NVCX_Topo, TopoUnexpectedError
+
+# ---------------------------------------------------------------------------
+# Helpers
+# ---------------------------------------------------------------------------
+
+
+def make_dev(
+    bdf_str: str,
+    vpd_sn: Optional[str] = None,
+    parent=None,
+    has_ats: bool = False,
+    subsystems: Optional[Dict] = None,
+) -> MagicMock:
+    """Return a mock PCIDevice for use with NVCX_Topo code."""
+    dev = MagicMock(name=f"PCIDevice({bdf_str})")
+    seg, bus, rest = bdf_str.split(":")
+    d, func = rest.split(".")
+    dev.bdf = PCIBDF(seg, bus, d, func)
+    dev.vpd_sn = vpd_sn
+    dev.parent = parent
+    dev.has_ats = has_ats
+    dev.get_subsystems.return_value = subsystems or {}
+    return dev
+
+
+def make_parent(bdf_str: str) -> MagicMock:
+    """Return a mock parent PCIDevice (used as NIC.parent)."""
+    p = MagicMock(name=f"ParentDevice({bdf_str})")
+    seg, bus, rest = bdf_str.split(":")
+    d, func = rest.split(".")
+    p.bdf = PCIBDF(seg, bus, d, func)
+    return p
+
+
+def make_nic(
+    pf_bdfs: List[str],
+    parent_bdf: Optional[str] = None,
+    vpd_sn: Optional[str] = None,
+    has_ats: bool = False,
+    subsystems: Optional[Dict] = None,
+) -> NVCX_Topo.NIC:
+    """Construct a NVCX_Topo.NIC from mock PCIDevices."""
+    parent = make_parent(parent_bdf) if parent_bdf else None
+    devs = [
+        make_dev(
+            bdf, vpd_sn=vpd_sn, parent=parent, has_ats=has_ats, subsystems=subsystems
+        )
+        for bdf in pf_bdfs
+    ]
+    return NVCX_Topo.NIC(set(devs))
+
+
+# ---------------------------------------------------------------------------
+# NVCX_Topo.NIC — constructor
+# ---------------------------------------------------------------------------
+
+
+class TestNVCX_Topo_NIC_Constructor:
+    def test_single_pf_no_parent(self):
+        nic = make_nic(["0000:00:01.0"], parent_bdf=None, vpd_sn=None)
+        assert nic.parent is None
+        assert nic.vpd_sn is None
+        assert len(nic.pfs) == 1
+
+    def test_single_pf_with_parent_and_sn(self):
+        nic = make_nic(["0000:00:01.0"], parent_bdf="0000:00:00.0", vpd_sn="SN123")
+        assert str(nic.parent.bdf) == "0000:00:00.0"
+        assert nic.vpd_sn == "SN123"
+
+    def test_multi_pf_same_parent_same_sn(self):
+        nic = make_nic(
+            ["0000:00:01.0", "0000:00:02.0"],
+            parent_bdf="0000:00:00.0",
+            vpd_sn="SN123",
+        )
+        assert len(nic.pfs) == 2
+
+    def test_multi_pf_different_parent_raises(self):
+        parent_a = make_parent("0000:00:00.0")
+        parent_b = make_parent("0000:00:10.0")
+        dev_a = make_dev("0000:00:01.0", parent=parent_a, vpd_sn="SN")
+        dev_b = make_dev("0000:00:02.0", parent=parent_b, vpd_sn="SN")
+        with pytest.raises(TopoUnexpectedError, match="same parent"):
+            NVCX_Topo.NIC({dev_a, dev_b})
+
+    def test_multi_pf_different_sn_raises(self):
+        parent = make_parent("0000:00:00.0")
+        dev_a = make_dev("0000:00:01.0", parent=parent, vpd_sn="SN1")
+        dev_b = make_dev("0000:00:02.0", parent=parent, vpd_sn="SN2")
+        with pytest.raises(TopoUnexpectedError, match="same VPD SN"):
+            NVCX_Topo.NIC({dev_a, dev_b})
+
+
+# ---------------------------------------------------------------------------
+# NVCX_Topo.NIC — primary_pf
+# ---------------------------------------------------------------------------
+
+
+class TestNVCX_Topo_NIC_PrimaryPf:
+    def test_multi_pf_returns_min_bdf(self):
+        parent = make_parent("0000:00:00.0")
+        dev_lo = make_dev("0000:00:01.0", parent=parent, vpd_sn="SN")
+        dev_hi = make_dev("0000:00:02.0", parent=parent, vpd_sn="SN")
+        nic = NVCX_Topo.NIC({dev_lo, dev_hi})
+        assert nic.primary_pf is dev_lo
+
+
+# ---------------------------------------------------------------------------
+# NVCX_Topo.NIC — to_dict
+# ---------------------------------------------------------------------------
+
+
+class TestNVCX_Topo_NIC_ToDict:
+    def test_has_parent_bdf(self):
+        nic = make_nic(["0000:00:01.0"], parent_bdf="0000:00:00.0", vpd_sn="SN123")
+        assert nic.to_dict()["parent_bdf"] == "0000:00:00.0"
+
+    def test_no_parent_gives_unknown(self):
+        nic = make_nic(["0000:00:01.0"], parent_bdf=None, vpd_sn=None)
+        assert nic.to_dict()["parent_bdf"] == "UNKNOWN"
+
+
+    def test_ats_taken_from_min_bdf_pf(self):
+        parent = make_parent("0000:00:00.0")
+        dev_lo = make_dev("0000:00:01.0", parent=parent, vpd_sn="SN", has_ats=False)
+        dev_hi = make_dev("0000:00:02.0", parent=parent, vpd_sn="SN", has_ats=True)
+        nic = NVCX_Topo.NIC({dev_lo, dev_hi})
+        assert nic.to_dict()["ats"] == False
+        nic = NVCX_Topo.NIC({dev_hi, dev_lo})
+        assert nic.to_dict()["ats"] == False
+
+
+# ---------------------------------------------------------------------------
+# NVCX_Topo.NIC — __str__
+# ---------------------------------------------------------------------------
+
+
+class TestNVCX_Topo_NIC_Str:
+    def test_single_pf_starts_with_nic_parent_header(self):
+        nic = make_nic(["0000:00:01.0"], parent_bdf="0000:00:00.0", vpd_sn="SN123")
+        assert str(nic).startswith("RDMA NIC Parent=0000:00:00.0")
+
+    def test_single_pf_no_parent_shows_unknown(self):
+        nic = make_nic(["0000:00:01.0"], parent_bdf=None, vpd_sn=None)
+        assert str(nic).startswith("RDMA NIC Parent=UNKNOWN")
+
+    def test_single_pf_ats_no(self):
+        nic = make_nic(["0000:00:01.0"], parent_bdf="0000:00:00.0", vpd_sn="SN123")
+        assert "NIC ATS: no" in str(nic)
+
+    def test_single_pf_ats_yes(self):
+        nic = make_nic(
+            ["0000:00:01.0"],
+            parent_bdf="0000:00:00.0",
+            vpd_sn="SN123",
+            has_ats=True,
+        )
+        assert "NIC ATS: yes" in str(nic)
+
+    def test_no_trailing_newline(self):
+        nic = make_nic(["0000:00:01.0"], parent_bdf="0000:00:00.0", vpd_sn="SN123")
+        assert not str(nic).endswith("\n")
+
+    def test_single_pf_with_infiniband(self):
+        nic = make_nic(
+            ["0000:00:01.0"],
+            parent_bdf="0000:00:00.0",
+            vpd_sn="SN123",
+            subsystems={"infiniband": {"mlx5_0"}},
+        )
+        assert "RDMA device: mlx5_0" in str(nic)
+
+    def test_single_pf_with_net(self):
+        nic = make_nic(
+            ["0000:00:01.0"],
+            parent_bdf="0000:00:00.0",
+            vpd_sn="SN123",
+            subsystems={"net": {"eth0"}},
+        )
+        assert "Net device: eth0" in str(nic)
+
+    def test_multi_pf_shows_sorted_pci_device_list(self):
+        nic = make_nic(
+            ["0000:00:02.0", "0000:00:01.0"],
+            parent_bdf="0000:00:00.0",
+            vpd_sn="SN123",
+        )
+        assert "NIC PCI devices: 0000:00:01.0, 0000:00:02.0" in str(nic)
+
+    def test_multi_pf_subsystems_merged(self):
+        parent = make_parent("0000:00:00.0")
+        dev1 = make_dev(
+            "0000:00:01.0",
+            parent=parent,
+            vpd_sn="SN",
+            subsystems={"infiniband": {"mlx5_0"}, "net": {"eth0"}},
+        )
+        dev2 = make_dev(
+            "0000:00:02.0",
+            parent=parent,
+            vpd_sn="SN",
+            subsystems={"infiniband": {"mlx5_1"}, "net": {"eth1"}},
+        )
+        nic = NVCX_Topo.NIC({dev1, dev2})
+        result = str(nic)
+        assert "RDMA devices: mlx5_0, mlx5_1" in result
+        assert "Net devices: eth0, eth1" in result
+
+
+# ---------------------------------------------------------------------------
+# NVCX_Topo.Board — constructor
+# ---------------------------------------------------------------------------
+
+
+class TestNVCX_Topo_Board_Constructor:
+    def test_single_nic_sn_stored(self):
+        nic = make_nic(["0000:00:01.0"], parent_bdf="0000:00:00.0", vpd_sn="SN123")
+        board = NVCX_Topo.Board({nic})
+        assert board.sn == "SN123"
+        assert nic in board.nics
+
+    def test_multi_nic_same_sn(self):
+        nic_a = make_nic(["0000:00:01.0"], parent_bdf="0000:00:00.0", vpd_sn="SN123")
+        nic_b = make_nic(["0000:00:11.0"], parent_bdf="0000:00:10.0", vpd_sn="SN123")
+        board = NVCX_Topo.Board({nic_a, nic_b})
+        assert len(board.nics) == 2
+        assert board.sn == "SN123"
+
+    def test_multi_nic_different_sn_raises(self):
+        nic_a = make_nic(["0000:00:01.0"], parent_bdf="0000:00:00.0", vpd_sn="SN1")
+        nic_b = make_nic(["0000:00:11.0"], parent_bdf="0000:00:10.0", vpd_sn="SN2")
+        with pytest.raises(TopoUnexpectedError, match="same VPD SN"):
+            NVCX_Topo.Board({nic_a, nic_b})
+
+
+# ---------------------------------------------------------------------------
+# NVCX_Topo.Board — to_dict
+# ---------------------------------------------------------------------------
+
+
+class TestNVCX_Topo_Board_ToDict:
+    def test_has_board_sn(self):
+        nic = make_nic(["0000:00:01.0"], parent_bdf="0000:00:00.0", vpd_sn="SN123")
+        board = NVCX_Topo.Board({nic})
+        assert board.to_dict()["board_sn"] == "SN123"
+
+    def test_none_sn_gives_unknown(self):
+        nic = make_nic(["0000:00:01.0"], parent_bdf=None, vpd_sn=None)
+        board = NVCX_Topo.Board({nic})
+        assert board.to_dict()["board_sn"] == "UNKNOWN"
+
+    def test_nics_list_length(self):
+        nic_a = make_nic(["0000:00:01.0"], parent_bdf="0000:00:00.0", vpd_sn="SN123")
+        nic_b = make_nic(["0000:00:11.0"], parent_bdf="0000:00:10.0", vpd_sn="SN123")
+        board = NVCX_Topo.Board({nic_a, nic_b})
+        assert len(board.to_dict()["nics"]) == 2
+
+    def test_nics_list_contains_nic_dicts(self):
+        nic = make_nic(
+            ["0000:00:01.0", "0000:00:02.0"],
+            parent_bdf="0000:00:00.0",
+            vpd_sn="SN123",
+        )
+        board = NVCX_Topo.Board({nic})
+        nic_dict = board.to_dict()["nics"][0]
+        assert nic_dict == nic.to_dict()
+
+
+# ---------------------------------------------------------------------------
+# NVCX_Topo.Board — __str__
+# ---------------------------------------------------------------------------
+
+
+class TestNVCX_Topo_Board_Str:
+    def test_single_nic_starts_with_board_header(self):
+        nic = make_nic(["0000:00:01.0"], parent_bdf="0000:00:00.0", vpd_sn="SN123")
+        board = NVCX_Topo.Board({nic})
+        assert str(board).startswith("RDMA NIC Board=SN123")
+
+    def test_none_sn_shows_unknown(self):
+        nic = make_nic(["0000:00:01.0"], parent_bdf=None, vpd_sn=None)
+        board = NVCX_Topo.Board({nic})
+        assert str(board).startswith("RDMA NIC Board=UNKNOWN")
+
+    def test_multi_nic_includes_nic_parent_headers(self):
+        nic_a = make_nic(["0000:00:01.0"], parent_bdf="0000:00:00.0", vpd_sn="SN123")
+        nic_b = make_nic(["0000:00:11.0"], parent_bdf="0000:00:10.0", vpd_sn="SN123")
+        board = NVCX_Topo.Board({nic_a, nic_b})
+        result = str(board)
+        assert "RDMA NIC Parent=0000:00:00.0" in result
+        assert "RDMA NIC Parent=0000:00:10.0" in result
+
+    def test_multi_nic_nic_body_indented(self):
+        nic_a = make_nic(["0000:00:01.0"], parent_bdf="0000:00:00.0", vpd_sn="SN123")
+        nic_b = make_nic(["0000:00:11.0"], parent_bdf="0000:00:10.0", vpd_sn="SN123")
+        board = NVCX_Topo.Board({nic_a, nic_b})
+        result = str(board)
+        assert "\t\tNIC ATS: no" in result
+
+    def test_multi_nic_no_trailing_newline(self):
+        nic_a = make_nic(["0000:00:01.0"], parent_bdf="0000:00:00.0", vpd_sn="SN123")
+        nic_b = make_nic(["0000:00:11.0"], parent_bdf="0000:00:10.0", vpd_sn="SN123")
+        board = NVCX_Topo.Board({nic_a, nic_b})
+        assert not str(board).endswith("\n")
+
+
+# ---------------------------------------------------------------------------
+# NVCX_Topo — constructor
+# ---------------------------------------------------------------------------
+
+
+class TestNVCX_Topo_Constructor:
+    def test_single_pf_with_parent_and_sn_yields_one_board(self):
+        parent = make_parent("0000:00:00.0")
+        dev = make_dev("0000:00:01.0", vpd_sn="SN123", parent=parent)
+        topo = NVCX_Topo({dev})
+        assert len(topo.boards) == 1
+
+    def test_two_pfs_same_parent_same_sn_one_nic_one_board(self):
+        parent = make_parent("0000:00:00.0")
+        dev1 = make_dev("0000:00:01.0", vpd_sn="SN123", parent=parent)
+        dev2 = make_dev("0000:00:02.0", vpd_sn="SN123", parent=parent)
+        topo = NVCX_Topo({dev1, dev2})
+        assert len(topo.boards) == 1
+        assert len(topo.boards[0].nics) == 1
+        assert len(topo.boards[0].nics[0].pfs) == 2
+
+    def test_two_pfs_different_parent_same_sn_two_nics_one_board(self):
+        parent_a = make_parent("0000:00:00.0")
+        parent_b = make_parent("0000:00:10.0")
+        dev1 = make_dev("0000:00:01.0", vpd_sn="SN123", parent=parent_a)
+        dev2 = make_dev("0000:00:11.0", vpd_sn="SN123", parent=parent_b)
+        topo = NVCX_Topo({dev1, dev2})
+        assert len(topo.boards) == 1
+        assert len(topo.boards[0].nics) == 2
+
+    def test_two_pfs_different_sn_two_boards(self):
+        parent_a = make_parent("0000:00:00.0")
+        parent_b = make_parent("0000:00:10.0")
+        dev1 = make_dev("0000:00:01.0", vpd_sn="SN1", parent=parent_a)
+        dev2 = make_dev("0000:00:11.0", vpd_sn="SN2", parent=parent_b)
+        topo = NVCX_Topo({dev1, dev2})
+        assert len(topo.boards) == 2
+
+
+# ---------------------------------------------------------------------------
+# NVCX_Topo — pfs / primary_pf
+# ---------------------------------------------------------------------------
+
+
+class TestNVCX_Topo_Pfs:
+    def test_pfs_returns_all_devs(self):
+        parent = make_parent("0000:00:00.0")
+        dev1 = make_dev("0000:00:01.0", vpd_sn="SN123", parent=parent)
+        dev2 = make_dev("0000:00:02.0", vpd_sn="SN123", parent=parent)
+        topo = NVCX_Topo({dev1, dev2})
+        assert topo.pfs == {dev1, dev2}
+
+    def test_primary_pf_is_min_bdf(self):
+        parent = make_parent("0000:00:00.0")
+        dev_lo = make_dev("0000:00:01.0", vpd_sn="SN123", parent=parent)
+        dev_hi = make_dev("0000:00:02.0", vpd_sn="SN123", parent=parent)
+        topo = NVCX_Topo({dev_lo, dev_hi})
+        assert topo.primary_pf is dev_lo
+
+    def test_primary_pf_across_boards(self):
+        parent_a = make_parent("0000:00:00.0")
+        parent_b = make_parent("0000:00:10.0")
+        dev_lo = make_dev("0000:00:01.0", vpd_sn="SN1", parent=parent_a)
+        dev_hi = make_dev("0000:00:11.0", vpd_sn="SN2", parent=parent_b)
+        topo = NVCX_Topo({dev_lo, dev_hi})
+        assert topo.primary_pf is dev_lo
+
+
+# ---------------------------------------------------------------------------
+# NVCX_Topo — to_dict
+# ---------------------------------------------------------------------------
+
+
+class TestNVCX_Topo_ToDict:
+    def test_single_pf_returns_flat_dict(self):
+        parent = make_parent("0000:00:00.0")
+        dev = make_dev("0000:00:01.0", vpd_sn="SN123", parent=parent)
+        topo = NVCX_Topo({dev})
+        result = topo.to_dict()
+        assert result["rdma_nic_pf_bdf"] == "0000:00:01.0"
+        assert result["rdma_nic_ats"] == False
+
+    def test_single_board_multi_pf_nic(self):
+        parent = make_parent("0000:00:00.0")
+        dev1 = make_dev("0000:00:01.0", vpd_sn="SN123", parent=parent)
+        dev2 = make_dev("0000:00:02.0", vpd_sn="SN123", parent=parent)
+        topo = NVCX_Topo({dev1, dev2})
+        result = topo.to_dict()
+        boards = result["rdma_nic_boards"]
+        assert len(boards) == 1
+        board = boards[0]
+        assert board["board_sn"] == "SN123"
+        assert len(board["nics"]) == 1
+        nic_dict = board["nics"][0]
+        assert nic_dict["parent_bdf"] == "0000:00:00.0"
+        assert "ats" in nic_dict
+        assert nic_dict["pf_bdfs"] == ["0000:00:01.0", "0000:00:02.0"]
+
+    def test_multi_board_multi_pf_nics(self):
+        parent_a = make_parent("0000:00:00.0")
+        parent_b = make_parent("0000:00:10.0")
+        dev1 = make_dev("0000:00:01.0", vpd_sn="SN1", parent=parent_a)
+        dev2 = make_dev("0000:00:02.0", vpd_sn="SN1", parent=parent_a)
+        dev3 = make_dev("0000:00:11.0", vpd_sn="SN2", parent=parent_b)
+        dev4 = make_dev("0000:00:12.0", vpd_sn="SN2", parent=parent_b)
+        topo = NVCX_Topo({dev1, dev2, dev3, dev4})
+        result = topo.to_dict()
+        assert len(result["rdma_nic_boards"]) == 2
+
+    def test_single_board_multi_nic_multi_pf(self):
+        parent_a = make_parent("0000:00:00.0")
+        parent_b = make_parent("0000:00:10.0")
+        dev1 = make_dev("0000:00:01.0", vpd_sn="SN123", parent=parent_a)
+        dev2 = make_dev("0000:00:02.0", vpd_sn="SN123", parent=parent_a)
+        dev3 = make_dev("0000:00:11.0", vpd_sn="SN123", parent=parent_b)
+        dev4 = make_dev("0000:00:12.0", vpd_sn="SN123", parent=parent_b)
+        topo = NVCX_Topo({dev1, dev2, dev3, dev4})
+        result = topo.to_dict()
+        boards = result["rdma_nic_boards"]
+        assert len(boards) == 1
+        assert len(boards[0]["nics"]) == 2
+
+    def test_boards_sorted_by_sn_in_to_dict(self):
+        parent_a = make_parent("0000:00:00.0")
+        parent_b = make_parent("0000:00:10.0")
+        dev_b = make_dev("0000:00:11.0", vpd_sn="SN_B", parent=parent_b)
+        dev_a = make_dev("0000:00:01.0", vpd_sn="SN_A", parent=parent_a)
+        topo = NVCX_Topo({dev_a, dev_b})
+        boards = topo.to_dict()["rdma_nic_boards"]
+        assert boards[0]["board_sn"] == "SN_A"
+        assert boards[1]["board_sn"] == "SN_B"
+
+
+# ---------------------------------------------------------------------------
+# NVCX_Topo — topo_str_key
+# ---------------------------------------------------------------------------
+
+
+class TestNVCX_Topo_TopoStrKey:
+    def test_single_pf_returns_nic_bdf(self):
+        parent = make_parent("0000:00:00.0")
+        dev = make_dev("0000:00:01.0", vpd_sn="SN123", parent=parent)
+        topo = NVCX_Topo({dev})
+        assert topo.topo_str_key() == "RDMA NIC=0000:00:01.0"
+
+    def test_multi_pf_returns_empty(self):
+        parent = make_parent("0000:00:00.0")
+        dev1 = make_dev("0000:00:01.0", vpd_sn="SN123", parent=parent)
+        dev2 = make_dev("0000:00:02.0", vpd_sn="SN123", parent=parent)
+        topo = NVCX_Topo({dev1, dev2})
+        assert topo.topo_str_key() == ""
+
+
+# ---------------------------------------------------------------------------
+# NVCX_Topo — topo_str
+# ---------------------------------------------------------------------------
+
+
+class TestNVCX_Topo_TopoStr:
+    def test_single_pf_contains_ats(self):
+        parent = make_parent("0000:00:00.0")
+        dev = make_dev("0000:00:01.0", vpd_sn="SN123", parent=parent)
+        topo = NVCX_Topo({dev})
+        result = topo.topo_str()
+        assert "NIC ATS: no" in result
+        assert not result.endswith("\n")
+
+    def test_single_board_single_nic_multi_pf(self):
+        parent = make_parent("0000:00:00.0")
+        dev1 = make_dev("0000:00:01.0", vpd_sn="SN123", parent=parent)
+        dev2 = make_dev("0000:00:02.0", vpd_sn="SN123", parent=parent)
+        topo = NVCX_Topo({dev1, dev2})
+        result = topo.topo_str()
+        assert "NIC ATS: no" in result
+        assert "NIC PCI devices: 0000:00:01.0, 0000:00:02.0" in result
+
+    def test_single_board_multi_nic_has_nic_parent_headers(self):
+        parent_a = make_parent("0000:00:00.0")
+        parent_b = make_parent("0000:00:10.0")
+        dev1 = make_dev("0000:00:01.0", vpd_sn="SN123", parent=parent_a)
+        dev2 = make_dev("0000:00:11.0", vpd_sn="SN123", parent=parent_b)
+        topo = NVCX_Topo({dev1, dev2})
+        result = topo.topo_str()
+        assert "RDMA NIC Parent=0000:00:00.0" in result
+        assert "RDMA NIC Parent=0000:00:10.0" in result
+
+    def test_multi_board_has_nic_parent_headers(self):
+        parent_a = make_parent("0000:00:00.0")
+        parent_b = make_parent("0000:00:10.0")
+        dev1 = make_dev("0000:00:01.0", vpd_sn="SN1", parent=parent_a)
+        dev2 = make_dev("0000:00:11.0", vpd_sn="SN2", parent=parent_b)
+        topo = NVCX_Topo({dev1, dev2})
+        result = topo.topo_str()
+        assert "RDMA NIC Parent=0000:00:00.0" in result
+        assert "RDMA NIC Parent=0000:00:10.0" in result
+
+    def test_multi_board_body_indented(self):
+        parent_a = make_parent("0000:00:00.0")
+        parent_b = make_parent("0000:00:10.0")
+        dev1 = make_dev("0000:00:01.0", vpd_sn="SN1", parent=parent_a)
+        dev2 = make_dev("0000:00:11.0", vpd_sn="SN2", parent=parent_b)
+        topo = NVCX_Topo({dev1, dev2})
+        result = topo.topo_str()
+        assert "\t\t\tNIC ATS: no" in result
+
+    def test_multi_board_no_trailing_newline(self):
+        parent_a = make_parent("0000:00:00.0")
+        parent_b = make_parent("0000:00:10.0")
+        dev1 = make_dev("0000:00:01.0", vpd_sn="SN1", parent=parent_a)
+        dev2 = make_dev("0000:00:11.0", vpd_sn="SN2", parent=parent_b)
+        topo = NVCX_Topo({dev1, dev2})
+        assert not topo.topo_str().endswith("\n")
+
+
+# ---------------------------------------------------------------------------
+# NVCX_Topo — board / NIC ordering
+# ---------------------------------------------------------------------------
+
+
+class TestNVCX_Topo_Ordering:
+    def test_boards_sorted_by_sn(self):
+        parent_a = make_parent("0000:00:00.0")
+        parent_b = make_parent("0000:00:10.0")
+        dev_b = make_dev("0000:00:11.0", vpd_sn="SN_B", parent=parent_b)
+        dev_a = make_dev("0000:00:01.0", vpd_sn="SN_A", parent=parent_a)
+        topo = NVCX_Topo({dev_a, dev_b})
+        assert topo.boards[0].sn == "SN_A"
+        assert topo.boards[1].sn == "SN_B"
+
+    def test_board_nics_sorted_by_parent_bdf(self):
+        parent_lo = make_parent("0000:00:00.0")
+        parent_hi = make_parent("0000:00:10.0")
+        dev_hi = make_dev("0000:00:11.0", vpd_sn="SN", parent=parent_hi)
+        dev_lo = make_dev("0000:00:01.0", vpd_sn="SN", parent=parent_lo)
+        topo = NVCX_Topo({dev_lo, dev_hi})
+        board = topo.boards[0]
+        assert str(board.nics[0].parent.bdf) == "0000:00:00.0"
+        assert str(board.nics[1].parent.bdf) == "0000:00:10.0"
+
+    def test_board_nic_without_parent_sorts_after_nic_with_parent(self):
+        parent = make_parent("0000:00:00.0")
+        dev_parented = make_dev("0000:00:01.0", vpd_sn="SN", parent=parent)
+        dev_orphan = make_dev("0000:00:02.0", vpd_sn="SN", parent=None)
+        nic_parented = NVCX_Topo.NIC({dev_parented})
+        nic_orphan = NVCX_Topo.NIC({dev_orphan})
+        board = NVCX_Topo.Board({nic_parented, nic_orphan})
+        assert board.nics[0] is nic_parented
+        assert board.nics[1] is nic_orphan
diff --git a/kernel-headers/rdma/mana-abi.h b/kernel-headers/rdma/mana-abi.h
index a75bf32..32cbbfc 100644
--- a/kernel-headers/rdma/mana-abi.h
+++ b/kernel-headers/rdma/mana-abi.h
@@ -25,7 +25,7 @@
 
 struct mana_ib_create_cq {
 	__aligned_u64 buf_addr;
-	__u16	flags;
+	__u16	comp_mask;
 	__u16	reserved0;
 	__u32	reserved1;
 };
@@ -57,6 +57,17 @@
 	__u32 queue_id[4];
 };
 
+struct mana_ib_create_uc_qp {
+	__aligned_u64 queue_buf[3];
+	__u32 queue_size[3];
+	__u32 comp_mask;
+};
+
+struct mana_ib_create_uc_qp_resp {
+	__u32 queue_id[3];
+	__u32 reserved;
+};
+
 struct mana_ib_create_wq {
 	__aligned_u64 wq_buf_addr;
 	__u32 wq_buf_size;
@@ -87,4 +98,26 @@
 	struct rss_resp_entry entries[64];
 };
 
+enum mana_ib_ucontext_support {
+	MANA_IB_UCNTX_ALLOC_PDN_SUPPORT = 1 << 0,
+};
+
+struct mana_ib_alloc_ucontext_resp {
+	__aligned_u64 comp_mask;
+};
+
+enum mana_ib_create_pd_flags {
+	MANA_IB_PD_SHORT_PDN = 1 << 0,
+};
+
+struct mana_ib_alloc_pd {
+	__u32 comp_mask;
+	__u32 reserved;
+};
+
+struct mana_ib_alloc_pd_resp {
+	__u32 pdn;
+	__u32 reserved;
+};
+
 #endif
diff --git a/providers/mana/CMakeLists.txt b/providers/mana/CMakeLists.txt
index 05011be..3ae2b02 100644
--- a/providers/mana/CMakeLists.txt
+++ b/providers/mana/CMakeLists.txt
@@ -1,5 +1,5 @@
 rdma_shared_provider(mana libmana.map
-  1 1.0.${PACKAGE_VERSION}
+  1 1.1.${PACKAGE_VERSION}
   mana.c
   manadv.c
   qp.c
diff --git a/providers/mana/cq.c b/providers/mana/cq.c
index 367d5a2..09da80c 100644
--- a/providers/mana/cq.c
+++ b/providers/mana/cq.c
@@ -165,7 +165,7 @@
 
 	cmd_drv = &cmd.drv_payload;
 	cmd_drv->buf_addr = (uintptr_t)cq->buf;
-	cmd_drv->flags = flags;
+	cmd_drv->comp_mask = flags;
 	resp.cqid = UINT32_MAX;
 
 	ret = ibv_cmd_create_cq(context, cq->cqe, channel, comp_vector,
@@ -284,6 +284,25 @@
 	return produced;
 }
 
+static inline int handle_requester_cqe(struct mana_qp *qp, struct gdma_cqe *cqe, struct ibv_wc *wc)
+{
+	struct mana_gdma_queue *send_queue = mana_ib_get_sreq(qp);
+	struct shadow_wqe_header *wqe;
+	int produced = 0;
+
+	while (!produced && (wqe = shadow_queue_get_next_to_consume(&qp->shadow_sq)) != NULL) {
+		send_queue->cons_idx += wqe->posted_wqe_size_in_bu;
+		send_queue->cons_idx &= GDMA_QUEUE_OFFSET_MASK;
+		if (wqe->flags != MANA_NO_SIGNAL_WC) {
+			fill_verbs_from_shadow_wqe(qp, wc, wqe);
+			produced++;
+		}
+		shadow_queue_advance_consumer(&qp->shadow_sq);
+	}
+
+	return produced;
+}
+
 static inline int handle_rc_requester_cqe(struct mana_qp *qp, struct gdma_cqe *cqe,
 					  struct ibv_wc *wc, int nwc, bool *consumed)
 {
@@ -443,6 +462,8 @@
 		return handle_error_cqe(qp, cqe, wc, nwc, consumed);
 	else if (cqe->rdma_cqe.cqe_type == CQE_TYPE_ARMED_CMPL)
 		return handle_rc_requester_cqe(qp, cqe, wc, nwc, consumed);
+	else if (cqe->is_sq && cqe->rdma_cqe.cqe_type == CQE_TYPE_UD_SEND)
+		return handle_requester_cqe(qp, cqe, wc);
 	else
 		return handle_responder_cqe(qp, cqe, wc);
 }
diff --git a/providers/mana/doorbells.h b/providers/mana/doorbells.h
index d805cdb..7651e76 100644
--- a/providers/mana/doorbells.h
+++ b/providers/mana/doorbells.h
@@ -87,6 +87,16 @@
 	mmio_flush_writes();
 }
 
+/* Has HW already produced the CQE at this index? Owner bits are written by HW,
+ * so a match proves idx < cur_cqe. Only meaningful for idx >= cq->head.
+ */
+static inline bool gdma_cq_idx_produced(struct mana_cq *cq, uint32_t idx)
+{
+	struct gdma_cqe *cqe = ((struct gdma_cqe *)cq->buf) + (idx % cq->cqe);
+
+	return cqe->owner_bits == ((idx / cq->cqe) & CQ_OWNER_MASK);
+}
+
 static inline void gdma_ring_cq_doorbell(struct mana_cq *cq, uint8_t arm)
 {
 	union gdma_doorbell_entry e;
@@ -94,8 +104,14 @@
 	uint32_t max_credit = cq->cqe << (GDMA_CQE_OWNER_BITS - 1);
 
 	if (cq->poll_credit >= max_credit) {
-		// To address the use-case of ibv that re-arms the CQ without polling
-		cq->poll_credit++;
+		// To address the use-case of ibv that re-arms the CQ without polling.
+		// (prod_idx + poll_credit - max_credit) is the index last given to HW,
+		// so only claim the next one once HW has produced it. Nothing produced
+		// implies no notification fired, so the CQ is still armed.
+		if (gdma_cq_idx_produced(cq, prod_idx + cq->poll_credit - max_credit))
+			cq->poll_credit++;
+		else
+			return;
 	} else {
 		// Set index of already polled CQE for unarm
 		cq->poll_credit = max_credit - (arm ? 0 : 1);
diff --git a/providers/mana/libmana.map b/providers/mana/libmana.map
index ab66295..24af016 100644
--- a/providers/mana/libmana.map
+++ b/providers/mana/libmana.map
@@ -6,3 +6,8 @@
 		manadv_init_obj;
 	local: *;
 };
+
+MANA_1.1 {
+	global:
+		manadv_alloc_pd;
+} MANA_1.0;
diff --git a/providers/mana/man/CMakeLists.txt b/providers/mana/man/CMakeLists.txt
index 24f1859..3458443 100644
--- a/providers/mana/man/CMakeLists.txt
+++ b/providers/mana/man/CMakeLists.txt
@@ -2,4 +2,5 @@
   manadv.7.md
   manadv_init_obj.3.md
   manadv_set_context_attr.3.md
+  manadv_alloc_pd.3.md
 )
diff --git a/providers/mana/man/manadv_alloc_pd.3.md b/providers/mana/man/manadv_alloc_pd.3.md
new file mode 100644
index 0000000..b121ea2
--- /dev/null
+++ b/providers/mana/man/manadv_alloc_pd.3.md
@@ -0,0 +1,36 @@
+---
+layout: page
+title: manadv_alloc_pd
+section: 3
+tagline: Verbs
+---
+
+# NAME
+manadv_alloc_pd \- Create a MANA specific PD for the RDMA device context.
+
+# SYNOPSIS"
+```c
+#include <infiniband/manadv.h>
+
+struct ibv_pd *manadv_alloc_pd(struct ibv_context *context, uint32_t flags);
+```
+
+# DESCRIPTION
+**manadv_alloc_pd()** allocates a PD for the RDMA device context with additional
+creation flags.
+
+# ARGUMENTS
+*context*
+:	RDMA device context to work on.
+
+*flags*
+:	A bitwise OR of the various values described below.
+
+	MANADV_PD_FLAGS_SHORT_PDN:
+		allocates a PD with 16 bit PDN.
+
+# RETURN VALUE
+returns a pointer to the allocated PD, or NULL if the request fails.
+
+# AUTHORS
+Konstantin Taranov <kotaranov@microsoft.com>
diff --git a/providers/mana/man/manadv_init_obj.3.md b/providers/mana/man/manadv_init_obj.3.md
index 575ea34..a4dd712 100644
--- a/providers/mana/man/manadv_init_obj.3.md
+++ b/providers/mana/man/manadv_init_obj.3.md
@@ -48,6 +48,10 @@
 	void		*db_page;
 };
 
+struct manadv_pd {
+	uint32_t	pdn;
+};
+
 struct manadv_obj {
 	struct {
 		struct ibv_qp		*in;
@@ -63,6 +67,11 @@
 		struct ibv_wq		*in;
 		struct manadv_rwq	*out;
 	} rwq;
+
+	struct {
+		struct ibv_pd		*in;
+		struct manadv_pd	*out;
+	} pd;
 };
 ```
 
@@ -74,6 +83,7 @@
 	MANADV_OBJ_QP   = 1 << 0,
 	MANADV_OBJ_CQ   = 1 << 1,
 	MANADV_OBJ_RWQ  = 1 << 2,
+	MANADV_OBJ_PD   = 1 << 3,
 };
 ```
 # RETURN VALUE
diff --git a/providers/mana/mana.c b/providers/mana/mana.c
index a59248b..87411d4 100644
--- a/providers/mana/mana.c
+++ b/providers/mana/mana.c
@@ -21,9 +21,10 @@
 #include "mana.h"
 
 DECLARE_DRV_CMD(mana_alloc_ucontext, IB_USER_VERBS_CMD_GET_CONTEXT, empty,
-		empty);
+		mana_ib_alloc_ucontext_resp);
 
-DECLARE_DRV_CMD(mana_alloc_pd, IB_USER_VERBS_CMD_ALLOC_PD, empty, empty);
+DECLARE_DRV_CMD(mana_alloc_pd, IB_USER_VERBS_CMD_ALLOC_PD, mana_ib_alloc_pd,
+		mana_ib_alloc_pd_resp);
 
 static const struct verbs_match_ent hca_table[] = {
 	VERBS_DRIVER_ID(RDMA_DRIVER_MANA),
@@ -114,19 +115,34 @@
 	return ibv_cmd_query_port(context, port, attr, &cmd, sizeof(cmd));
 }
 
-struct ibv_pd *mana_alloc_pd(struct ibv_context *context)
+struct ibv_pd *mana_alloc_pd_ex(struct ibv_context *context, uint32_t flags)
 {
-	struct ibv_alloc_pd cmd;
-	struct mana_alloc_pd_resp resp;
+	struct mana_context *mctx = to_mctx(context);
+	struct mana_alloc_pd cmd = {};
+	struct mana_ib_alloc_pd *cmd_drv = &cmd.drv_payload;
+	struct mana_alloc_pd_resp resp = {};
+	size_t cmd_size = sizeof(cmd.ibv_cmd); /* v0 size */
 	struct mana_pd *pd;
 	int ret;
 
+	if ((flags & MANADV_PD_FLAGS_SHORT_PDN) &&
+	    !(mctx->comp_mask & MANA_IB_UCNTX_ALLOC_PDN_SUPPORT)) {
+		errno = EOPNOTSUPP;
+		return NULL;
+	}
+
 	pd = calloc(1, sizeof(*pd));
 	if (!pd)
 		return NULL;
 
-	ret = ibv_cmd_alloc_pd(context, &pd->ibv_pd, &cmd, sizeof(cmd),
-			       &resp.ibv_resp, sizeof(resp));
+	if (mctx->comp_mask & MANA_IB_UCNTX_ALLOC_PDN_SUPPORT)
+		cmd_size = sizeof(cmd); /* v1 size */
+
+	if (flags & MANADV_PD_FLAGS_SHORT_PDN)
+		cmd_drv->comp_mask |= MANA_IB_PD_SHORT_PDN;
+
+	ret = ibv_cmd_alloc_pd(context, &pd->ibv_pd, &cmd.ibv_cmd, cmd_size,
+			      &resp.ibv_resp, sizeof(resp));
 	if (ret) {
 		verbs_err(verbs_get_ctx(context), "Failed to allocate PD\n");
 		errno = ret;
@@ -134,9 +150,16 @@
 		return NULL;
 	}
 
+	pd->pdn = resp.pdn;
+
 	return &pd->ibv_pd;
 }
 
+static struct ibv_pd *mana_alloc_pd(struct ibv_context *context)
+{
+	return mana_alloc_pd_ex(context, 0);
+}
+
 struct ibv_pd *
 mana_alloc_parent_domain(struct ibv_context *context,
 			 struct ibv_parent_domain_init_attr *attr)
@@ -452,7 +475,7 @@
 {
 	int ret, i;
 	struct mana_context *context;
-	struct mana_alloc_ucontext_resp resp;
+	struct mana_alloc_ucontext_resp resp = {};
 	struct ibv_get_context cmd;
 
 	context = verbs_init_and_alloc_context(ibdev, cmd_fd, context, ibv_ctx,
@@ -468,6 +491,8 @@
 		goto free_ctx;
 	}
 
+	context->comp_mask = resp.drv_payload.comp_mask;
+
 	verbs_set_ops(&context->ibv_ctx, &mana_ctx_ops);
 
 	pthread_mutex_init(&context->qp_table_mutex, NULL);
diff --git a/providers/mana/mana.h b/providers/mana/mana.h
index 1f8a147..98318d1 100644
--- a/providers/mana/mana.h
+++ b/providers/mana/mana.h
@@ -49,7 +49,8 @@
 	USER_RNIC_SEND_QUEUE_RESPONDER = 1,
 	USER_RNIC_RECV_QUEUE_REQUESTER = 2,
 	USER_RNIC_RECV_QUEUE_RESPONDER = 3,
-	USER_RNIC_QUEUE_TYPE_MAX = 4,
+	USER_RNIC_SEND_QUEUE_MM = 4,
+	USER_RNIC_QUEUE_TYPE_MAX = 5,
 };
 
 #define QUEUE_TYPE_MASK 0x3
@@ -89,6 +90,7 @@
 	struct mana_table qp_rtable[MANA_QP_TABLE_SIZE];
 	struct mana_table qp_stable[MANA_QP_TABLE_SIZE];
 	pthread_mutex_t qp_table_mutex;
+	uint64_t comp_mask;
 
 	struct manadv_ctx_allocators extern_alloc;
 	void *db_page;
@@ -196,6 +198,7 @@
 
 struct mana_pd {
 	struct ibv_pd ibv_pd;
+	uint32_t pdn;
 	struct mana_pd *mprotection_domain;
 };
 
@@ -216,7 +219,7 @@
 int mana_query_port(struct ibv_context *context, uint8_t port,
 		    struct ibv_port_attr *attr);
 
-struct ibv_pd *mana_alloc_pd(struct ibv_context *context);
+struct ibv_pd *mana_alloc_pd_ex(struct ibv_context *context, uint32_t flags);
 struct ibv_pd *
 mana_alloc_parent_domain(struct ibv_context *context,
 			 struct ibv_parent_domain_init_attr *attr);
diff --git a/providers/mana/manadv.c b/providers/mana/manadv.c
index 4b40d05..2392adf 100644
--- a/providers/mana/manadv.c
+++ b/providers/mana/manadv.c
@@ -42,7 +42,7 @@
 
 int manadv_init_obj(struct manadv_obj *obj, uint64_t obj_type)
 {
-	if (obj_type & ~(MANADV_OBJ_QP | MANADV_OBJ_CQ | MANADV_OBJ_RWQ))
+	if (obj_type & ~(MANADV_OBJ_QP | MANADV_OBJ_CQ | MANADV_OBJ_RWQ | MANADV_OBJ_PD))
 		return EINVAL;
 
 	if (obj_type & MANADV_OBJ_QP) {
@@ -84,5 +84,17 @@
 		obj->rwq.out->db_page = ctx->db_page;
 	}
 
+	if (obj_type & MANADV_OBJ_PD) {
+		struct ibv_pd *ibpd = obj->pd.in;
+		struct mana_pd *pd = container_of(ibpd, struct mana_pd, ibv_pd);
+
+		obj->pd.out->pdn = pd->pdn;
+	}
+
 	return 0;
 }
+
+struct ibv_pd *manadv_alloc_pd(struct ibv_context *context, uint32_t flags)
+{
+	return mana_alloc_pd_ex(context, flags);
+}
diff --git a/providers/mana/manadv.h b/providers/mana/manadv.h
index 27c8fe9..87f6e8f 100644
--- a/providers/mana/manadv.h
+++ b/providers/mana/manadv.h
@@ -52,6 +52,10 @@
 	void *db_page;
 };
 
+struct manadv_pd {
+	uint32_t pdn;
+};
+
 struct manadv_obj {
 	struct {
 		struct ibv_qp *in;
@@ -67,16 +71,27 @@
 		struct ibv_wq *in;
 		struct manadv_rwq *out;
 	} rwq;
+	struct {
+		struct ibv_pd *in;
+		struct manadv_pd *out;
+	} pd;
 };
 
 enum manadv_obj_type {
 	MANADV_OBJ_QP = 1 << 0,
 	MANADV_OBJ_CQ = 1 << 1,
 	MANADV_OBJ_RWQ = 1 << 2,
+	MANADV_OBJ_PD = 1 << 3,
+};
+
+enum {
+	MANADV_PD_FLAGS_SHORT_PDN = 1 << 0,
 };
 
 int manadv_init_obj(struct manadv_obj *obj, uint64_t obj_type);
 
+struct ibv_pd *manadv_alloc_pd(struct ibv_context *context, uint32_t flags);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/providers/mana/qp.c b/providers/mana/qp.c
index b6a9a7e..752959b 100644
--- a/providers/mana/qp.c
+++ b/providers/mana/qp.c
@@ -31,6 +31,9 @@
 DECLARE_DRV_CMD(mana_create_rc_qp, IB_USER_VERBS_CMD_CREATE_QP,
 		mana_ib_create_rc_qp, mana_ib_create_rc_qp_resp);
 
+DECLARE_DRV_CMD(mana_create_uc_qp, IB_USER_VERBS_CMD_CREATE_QP,
+		mana_ib_create_uc_qp, mana_ib_create_uc_qp_resp);
+
 static struct ibv_qp *mana_create_qp_raw(struct ibv_pd *ibpd,
 					 struct ibv_qp_init_attr *attr)
 {
@@ -216,46 +219,142 @@
 	uint32_t size = 0;
 	uint32_t sges = 0;
 
-	if (attr->qp_type == IBV_QPT_RC) {
-		switch (type) {
-		case USER_RNIC_SEND_QUEUE_REQUESTER:
-			/* WQE must have at least one SGE */
-			/* For write with imm we need one extra SGE */
-			sges = max(1U, attr->cap.max_send_sge) + 1;
-			size = attr->cap.max_send_wr * get_large_wqe_size(sges);
-			break;
-		case USER_RNIC_SEND_QUEUE_RESPONDER:
-			size = MANA_PAGE_SIZE;
-			break;
-		case USER_RNIC_RECV_QUEUE_REQUESTER:
-			size = MANA_PAGE_SIZE;
-			break;
-		case USER_RNIC_RECV_QUEUE_RESPONDER:
-			/* WQE must have at least one SGE */
-			sges = max(1U, attr->cap.max_recv_sge);
-			size = attr->cap.max_recv_wr * get_wqe_size(sges);
-			break;
-		default:
-			return 0;
-		}
+	switch (type) {
+	case USER_RNIC_SEND_QUEUE_REQUESTER:
+		/* WQE must have at least one SGE */
+		/* For write with imm we need one extra SGE */
+		sges = max(1U, attr->cap.max_send_sge) + 1;
+		size = align_hw_size(attr->cap.max_send_wr * get_large_wqe_size(sges));
+		break;
+	case USER_RNIC_SEND_QUEUE_RESPONDER:
+		if (attr->qp_type == IBV_QPT_RC)
+			size = align_hw_size(MANA_PAGE_SIZE);
+		break;
+	case USER_RNIC_RECV_QUEUE_REQUESTER:
+		if (attr->qp_type == IBV_QPT_RC)
+			size = align_hw_size(MANA_PAGE_SIZE);
+		break;
+	case USER_RNIC_RECV_QUEUE_RESPONDER:
+		/* WQE must have at least one SGE */
+		sges = max(1U, attr->cap.max_recv_sge);
+		size = align_hw_size(attr->cap.max_recv_wr * get_wqe_size(sges));
+		break;
+	case USER_RNIC_SEND_QUEUE_MM:
+		sges = 2;
+		size = align_hw_size(attr->cap.max_send_wr * get_large_wqe_size(sges));
+		break;
+	default:
+		return 0;
 	}
 
-	size = align_hw_size(size);
-
 	if (attr->qp_type == IBV_QPT_RC && type == USER_RNIC_SEND_QUEUE_REQUESTER)
 		size += sizeof(struct mana_ib_rollback_shared_mem);
 
 	return size;
 }
 
-static struct ibv_qp *mana_create_qp_rnic(struct ibv_pd *ibpd,
-					  struct ibv_qp_init_attr *attr)
+static int mana_create_cmd_qp_rc(struct mana_qp *qp, struct ibv_pd *ibpd,
+				 struct ibv_qp_init_attr *attr)
 {
-	struct mana_context *ctx = to_mctx(ibpd->context);
 	struct mana_ib_create_rc_qp_resp *qp_resp_drv;
 	struct mana_create_rc_qp_resp qp_resp = {};
 	struct mana_ib_create_rc_qp *qp_cmd_drv;
 	struct mana_create_rc_qp qp_cmd = {};
+	int ret, i;
+
+	qp_cmd_drv = &qp_cmd.drv_payload;
+	qp_resp_drv = &qp_resp.drv_payload;
+
+	for (i = 0; i < USER_RNIC_QUEUE_TYPE_MAX; ++i) {
+		if (i == USER_RNIC_SEND_QUEUE_MM)
+			continue;
+		qp_cmd_drv->queue_buf[i] = (uintptr_t)qp->rnic_qp.queues[i].buffer;
+		qp_cmd_drv->queue_size[i] = qp->rnic_qp.queues[i].size;
+	}
+
+	ret = ibv_cmd_create_qp(ibpd, &qp->ibqp.qp, attr, &qp_cmd.ibv_cmd,
+				sizeof(qp_cmd), &qp_resp.ibv_resp,
+				sizeof(qp_resp));
+	if (ret) {
+		verbs_err(verbs_get_ctx(ibpd->context), "Create QP failed\n");
+		return ret;
+	}
+
+	for (i = 0; i < USER_RNIC_QUEUE_TYPE_MAX; ++i) {
+		if (i == USER_RNIC_SEND_QUEUE_MM)
+			continue;
+		qp->rnic_qp.queues[i].id = qp_resp_drv->queue_id[i];
+	}
+
+	return 0;
+}
+
+enum {
+	MANA_UC_UDATA_SQR = 0,
+	MANA_UC_UDATA_RQR = 1,
+	MANA_UC_UDATA_SMQ = 2,
+};
+
+static int mana_create_cmd_qp_uc(struct mana_qp *qp, struct ibv_pd *ibpd,
+				 struct ibv_qp_init_attr *attr)
+{
+	struct mana_ib_create_uc_qp_resp *qp_resp_drv;
+	struct mana_create_uc_qp_resp qp_resp = {};
+	struct mana_ib_create_uc_qp *qp_cmd_drv;
+	struct mana_create_uc_qp qp_cmd = {};
+	int ret;
+
+	qp_cmd_drv = &qp_cmd.drv_payload;
+	qp_resp_drv = &qp_resp.drv_payload;
+
+	qp_cmd_drv->queue_buf[MANA_UC_UDATA_SQR] =
+		(uintptr_t)qp->rnic_qp.queues[USER_RNIC_SEND_QUEUE_REQUESTER].buffer;
+	qp_cmd_drv->queue_size[MANA_UC_UDATA_SQR] =
+		qp->rnic_qp.queues[USER_RNIC_SEND_QUEUE_REQUESTER].size;
+
+	qp_cmd_drv->queue_buf[MANA_UC_UDATA_RQR] =
+		(uintptr_t)qp->rnic_qp.queues[USER_RNIC_RECV_QUEUE_RESPONDER].buffer;
+	qp_cmd_drv->queue_size[MANA_UC_UDATA_RQR] =
+		qp->rnic_qp.queues[USER_RNIC_RECV_QUEUE_RESPONDER].size;
+
+	qp_cmd_drv->queue_buf[MANA_UC_UDATA_SMQ] =
+		(uintptr_t)qp->rnic_qp.queues[USER_RNIC_SEND_QUEUE_MM].buffer;
+	qp_cmd_drv->queue_size[MANA_UC_UDATA_SMQ] =
+		qp->rnic_qp.queues[USER_RNIC_SEND_QUEUE_MM].size;
+
+	ret = ibv_cmd_create_qp(ibpd, &qp->ibqp.qp, attr, &qp_cmd.ibv_cmd,
+				sizeof(qp_cmd), &qp_resp.ibv_resp,
+				sizeof(qp_resp));
+	if (ret) {
+		verbs_err(verbs_get_ctx(ibpd->context), "Create QP failed\n");
+		return ret;
+	}
+
+	qp->rnic_qp.queues[USER_RNIC_SEND_QUEUE_REQUESTER].id =
+		qp_resp_drv->queue_id[MANA_UC_UDATA_SQR];
+	qp->rnic_qp.queues[USER_RNIC_RECV_QUEUE_RESPONDER].id =
+		qp_resp_drv->queue_id[MANA_UC_UDATA_RQR];
+	qp->rnic_qp.queues[USER_RNIC_SEND_QUEUE_MM].id =
+		qp_resp_drv->queue_id[MANA_UC_UDATA_SMQ];
+
+	return 0;
+}
+
+static int mana_create_cmd_qp(struct mana_qp *qp, struct ibv_pd *ibpd,
+			      struct ibv_qp_init_attr *attr)
+{
+	if (attr->qp_type == IBV_QPT_RC)
+		return mana_create_cmd_qp_rc(qp, ibpd, attr);
+	else if (attr->qp_type == IBV_QPT_UC)
+		return mana_create_cmd_qp_uc(qp, ibpd, attr);
+	else
+		return -EOPNOTSUPP;
+}
+
+static struct ibv_qp *mana_create_qp_rnic(struct ibv_pd *ibpd,
+					  struct ibv_qp_init_attr *attr)
+{
+	struct mana_context *ctx = to_mctx(ibpd->context);
 	struct mana_qp *qp;
 	int ret, i;
 
@@ -263,9 +362,6 @@
 	if (!qp)
 		return NULL;
 
-	qp_cmd_drv = &qp_cmd.drv_payload;
-	qp_resp_drv = &qp_resp.drv_payload;
-
 	pthread_spin_init(&qp->sq_lock, PTHREAD_PROCESS_PRIVATE);
 	pthread_spin_init(&qp->rq_lock, PTHREAD_PROCESS_PRIVATE);
 	qp->sq_sig_all = attr->sq_sig_all;
@@ -291,29 +387,18 @@
 
 		if (qp->rnic_qp.queues[i].size != 0 && !qp->rnic_qp.queues[i].buffer) {
 			verbs_err(verbs_get_ctx(ibpd->context),
-				  "Failed to allocate memory for RC queue %d\n", i);
+				  "Failed to allocate memory for queue %d\n", i);
 			errno = ENOMEM;
 			goto destroy_queues;
 		}
-
-		qp_cmd_drv->queue_buf[i] = (uintptr_t)qp->rnic_qp.queues[i].buffer;
-		qp_cmd_drv->queue_size[i] = qp->rnic_qp.queues[i].size;
 	}
 
-	mana_ib_init_rb_shmem(qp);
-
-	ret = ibv_cmd_create_qp(ibpd, &qp->ibqp.qp, attr, &qp_cmd.ibv_cmd,
-				sizeof(qp_cmd), &qp_resp.ibv_resp,
-				sizeof(qp_resp));
+	ret = mana_create_cmd_qp(qp, ibpd, attr);
 	if (ret) {
-		verbs_err(verbs_get_ctx(ibpd->context), "Create QP failed\n");
 		errno = ret;
-		goto free_rb;
+		goto destroy_queues;
 	}
 
-	for (i = 0; i < USER_RNIC_QUEUE_TYPE_MAX; ++i)
-		qp->rnic_qp.queues[i].id = qp_resp_drv->queue_id[i];
-
 	qp->ibqp.qp.qp_num = qp->rnic_qp.queues[USER_RNIC_RECV_QUEUE_RESPONDER].id;
 
 	ret = mana_store_qp(ctx, qp);
@@ -322,12 +407,12 @@
 		goto destroy_qp;
 	}
 
+	mana_ib_init_rb_shmem(qp);
+
 	return &qp->ibqp.qp;
 
 destroy_qp:
 	ibv_cmd_destroy_qp(&qp->ibqp.qp);
-free_rb:
-	mana_ib_deinit_rb_shmem(qp);
 destroy_queues:
 	while (i-- > 0)
 		mana_dealloc_mem(qp->rnic_qp.queues[i].buffer, qp->rnic_qp.queues[i].size);
@@ -346,6 +431,7 @@
 	case IBV_QPT_RAW_PACKET:
 		return mana_create_qp_raw(ibpd, attr);
 	case IBV_QPT_RC:
+	case IBV_QPT_UC:
 		return mana_create_qp_rnic(ibpd, attr);
 	default:
 		verbs_err(verbs_get_ctx(ibpd->context),
@@ -382,7 +468,8 @@
 			if (attr_mask & IBV_QP_SQ_PSN) {
 				qp->sq_ssn = 1;
 				qp->sq_psn = attr->sq_psn;
-				gdma_arm_normal_cqe(mana_ib_get_rreq(qp), attr->sq_psn);
+				if (qp->ibqp.qp.qp_type == IBV_QPT_RC)
+					gdma_arm_normal_cqe(mana_ib_get_rreq(qp), attr->sq_psn);
 			}
 			break;
 		default:
@@ -397,7 +484,7 @@
 	struct ibv_modify_qp cmd = {};
 	int err;
 
-	if (ibqp->qp_type != IBV_QPT_RC)
+	if (ibqp->qp_type != IBV_QPT_RC && ibqp->qp_type != IBV_QPT_UC)
 		return EOPNOTSUPP;
 
 	pthread_spin_lock(&qp->sq_lock);
@@ -443,7 +530,7 @@
 	struct mana_context *ctx = to_mctx(ibqp->context);
 	int ret, i;
 
-	if (ibqp->qp_type == IBV_QPT_RC) {
+	if (ibqp->qp_type == IBV_QPT_RC || ibqp->qp_type == IBV_QPT_UC) {
 		mana_remove_qp(ctx, qp);
 		mana_drain_cqes(qp);
 	}
@@ -459,6 +546,7 @@
 		ctx->extern_alloc.free(qp->raw_qp.send_buf, ctx->extern_alloc.data);
 		break;
 	case IBV_QPT_RC:
+	case IBV_QPT_UC:
 		pthread_spin_destroy(&qp->sq_lock);
 		pthread_spin_destroy(&qp->rq_lock);
 		destroy_shadow_queue(&qp->shadow_sq);
diff --git a/providers/mana/rollback.h b/providers/mana/rollback.h
index 59621a5..c908f3f 100644
--- a/providers/mana/rollback.h
+++ b/providers/mana/rollback.h
@@ -39,6 +39,8 @@
 
 static inline void mana_ib_init_rb_shmem(struct mana_qp *qp)
 {
+	if (qp->ibqp.qp.qp_type != IBV_QPT_RC)
+		return;
 	// take some bytes for rollback memory
 	struct mana_gdma_queue *req_sq =
 		&qp->rnic_qp.queues[USER_RNIC_SEND_QUEUE_REQUESTER];
@@ -54,6 +56,8 @@
 
 static inline void mana_ib_deinit_rb_shmem(struct mana_qp *qp)
 {
+	if (qp->ibqp.qp.qp_type != IBV_QPT_RC)
+		return;
 	// return back bytes for rollback memory
 	struct mana_gdma_queue *req_sq =
 		&qp->rnic_qp.queues[USER_RNIC_SEND_QUEUE_REQUESTER];
@@ -62,6 +66,9 @@
 
 static inline void mana_ib_reset_rb_shmem(struct mana_qp *qp)
 {
+	if (qp->ibqp.qp.qp_type != IBV_QPT_RC)
+		return;
+
 	struct mana_ib_rollback_shared_mem *rb_shmem =
 		mana_ib_get_rollback_sh_mem(qp);
 
@@ -71,6 +78,9 @@
 
 static inline void mana_ib_update_shared_mem_right_offset(struct mana_qp *qp, uint32_t offset_in_bu)
 {
+	if (qp->ibqp.qp.qp_type != IBV_QPT_RC)
+		return;
+
 	struct mana_ib_rollback_shared_mem *rb_shmem =
 			mana_ib_get_rollback_sh_mem(qp);
 
diff --git a/providers/mana/wr.c b/providers/mana/wr.c
index 88ee8ad..e4d7c38 100644
--- a/providers/mana/wr.c
+++ b/providers/mana/wr.c
@@ -191,6 +191,7 @@
 {
 	switch (ibqp->qp_type) {
 	case IBV_QPT_RC:
+	case IBV_QPT_UC:
 		return mana_ib_post_recv(ibqp, wr, bad);
 	default:
 		verbs_err(verbs_get_ctx(ibqp->context), "QPT not supported %d\n", ibqp->qp_type);
@@ -350,7 +351,7 @@
 			       &send_oob, oob_sge, num_sge, MTU_SIZE(qp->mtu), flags, &gdma_wqe);
 	if (ret) {
 		verbs_err(verbs_get_ctx(qp->ibqp.qp.context),
-			  "rc post send error, ret %d\n", ret);
+			  "post send error, ret %d\n", ret);
 		goto cleanup;
 	}
 
@@ -430,6 +431,7 @@
 {
 	switch (ibqp->qp_type) {
 	case IBV_QPT_RC:
+	case IBV_QPT_UC:
 		return mana_ib_post_send(ibqp, wr, bad);
 	default:
 		verbs_err(verbs_get_ctx(ibqp->context), "QPT not supported %d\n", ibqp->qp_type);
diff --git a/providers/mlx4/cq.c b/providers/mlx4/cq.c
index 61313b8..e17ae22 100644
--- a/providers/mlx4/cq.c
+++ b/providers/mlx4/cq.c
@@ -700,7 +700,8 @@
 	 * Now sweep backwards through the CQ, removing CQ entries
 	 * that match our QP by copying older entries on top of them.
 	 */
-	while ((int) --prod_index - (int) cq->cons_index >= 0) {
+	while (prod_index != cq->cons_index) {
+		--prod_index;
 		cqe = get_cqe(cq, prod_index & cq->verbs_cq.cq.cqe);
 		cqe += cqe_inc;
 		if (srq && srq->ext_srq &&
diff --git a/providers/mlx5/cq.c b/providers/mlx5/cq.c
index eeaf4e6..f892658 100644
--- a/providers/mlx5/cq.c
+++ b/providers/mlx5/cq.c
@@ -1842,7 +1842,8 @@
 	 * that match our QP by copying older entries on top of them.
 	 */
 	cqe_version = (to_mctx(cq->verbs_cq.cq.context))->cqe_version;
-	while ((int) --prod_index - (int) cq->cons_index >= 0) {
+	while (prod_index != cq->cons_index) {
+		--prod_index;
 		cqe = get_cqe(cq, prod_index & cq->verbs_cq.cq.cqe);
 		cqe64 = (cq->cqe_sz == 64) ? cqe : cqe + 64;
 		if (free_res_cqe(cqe64, rsn, srq, cqe_version)) {
diff --git a/providers/mlx5/dbrec.c b/providers/mlx5/dbrec.c
index c5d1c3f..ae99cd9 100644
--- a/providers/mlx5/dbrec.c
+++ b/providers/mlx5/dbrec.c
@@ -105,8 +105,10 @@
 				   mparent_domain->pd_context, 8, 8,
 				   MLX5DV_RES_TYPE_DBR);
 
-		if (db == IBV_ALLOCATOR_USE_DEFAULT)
+		if (db == IBV_ALLOCATOR_USE_DEFAULT) {
+			db = NULL;
 			goto default_alloc;
+		}
 
 		if (!db)
 			return NULL;
diff --git a/providers/mthca/cq.c b/providers/mthca/cq.c
index dd8baca..3fff17f 100644
--- a/providers/mthca/cq.c
+++ b/providers/mthca/cq.c
@@ -563,7 +563,8 @@
 	 * Now sweep backwards through the CQ, removing CQ entries
 	 * that match our QP by copying older entries on top of them.
 	 */
-	while ((int) --prod_index - (int) cq->cons_index >= 0) {
+	while (prod_index != cq->cons_index) {
+		--prod_index;
 		cqe = get_cqe(cq, prod_index & cq->ibv_cq.cqe);
 		if (cqe->my_qpn == htobe32(qpn)) {
 			if (srq && is_recv_cqe(cqe))
diff --git a/pyverbs/__init__.py b/pyverbs/__init__.py
index e69de29..9226fe7 100644
--- a/pyverbs/__init__.py
+++ b/pyverbs/__init__.py
@@ -0,0 +1 @@
+from .__version__ import __version__
diff --git a/pyverbs/__version__.py.in b/pyverbs/__version__.py.in
new file mode 100644
index 0000000..280beb2
--- /dev/null
+++ b/pyverbs/__version__.py.in
@@ -0,0 +1 @@
+__version__ = "@PACKAGE_VERSION@"