ASAravind Sivakumaran
Theme

Theme and appearance

Appearance
Palette

Pick any colour. The displayed accent adjusts for readable contrast in light or dark mode.

← PCIe guide

PCIe / BRIDGES AND ATOMICS

Why does it work in one slot and not the other?

A request from a card rarely goes straight to the CPU. It passes through bridges: the root port, and often the ports of a switch on the motherboard or the card itself. Think of them as sorting offices on a parcel's route. Every office must know how to handle the kind of parcel you send, or the parcel stops there. The card can support PASID or AtomicOps perfectly and still not get to use them, because one office on its route does not.

This chapter puts a switch between the root port and the emulated card, shows how bridges route requests, which features every port on the path must support, and the AtomicOps themselves, to try.

What claims does this page make? 3 points
  • Spec rule: AtomicOps and PASID need support at every port on the path, and AtomicOps come in FetchAdd, Swap and CAS with natural alignment (PCI Express Base Specification, members only, so not checked here).
  • Platform assumption: whether a given root port or switch completes AtomicOps or carries prefixes depends on the chip and board, and the checks quoted are Linux's, which can change between kernel versions.
  • Teaching model: the root port, switch and card (vendor 1ee7) are made up, and the N100 section is one reader's lspci, not every N100.

The tree: lspci -tv

lspci -tv draws the bridges. For the card behind a switch it prints one line. Press each part:

-[]------------------

Nothing selected yet.

A switch is not one device but several bridges: one upstream port facing the root and a downstream port for each link below, joined by an internal bus. AMD's motherboard chipsets are switches, and so is the first thing above the GPU on recent AMD Radeon cards: lspci lists an Upstream Port and a Downstream Port of a PCI Express switch before the GPU itself. A card plugged straight into a CPU slot has only the root port above it; a device built into the processor (bus 00) has none.

The CPU and OS reach the card through the root port and a switch. The card's requests go up through the switch and the root port to the IOMMU, then to memory. CPU + OSconfig reads, MMIO IOMMUin the root complex MemoryRAM Root port00:01.1, the link Switchports 01:00.0, 02:00.0 Card 03:00.0config, BARs

Experiments

One platform, chosen for teaching: a PC whose IOMMU sits in the root complex, as on AMD and Intel systems.

Experiment

    Registers, as lspci prints them

      Commands and reads

        Notes

          A bridge in lspci

          A bridge's configuration space has a different header (type 1). Instead of the card's six BARs it has the bus numbers and the address windows that decide what it passes down:

          00:01.1 Class 0604: Device 1ee7:0100 (rev 01)
          	Bus: primary=00, secondary=01, subordinate=03, sec-latency=0
          	Memory behind bridge: fe800000-fe8fffff [size=1M] [32-bit]
          	Prefetchable memory behind bridge: 00000000fff00000-00000000000fffff [disabled] [64-bit]
          	Capabilities: [40] Express (v2) Root Port (Slot-), IntMsgNum 0
          		DevCap2: … ExtFmt+ EETLPPrefix+, MaxEETLPPrefixes 4
          			 AtomicOpsCap: Routing+ 32bit+ 64bit+ 128bitCAS-
          		DevCtl2: …
          			 AtomicOpsCtl: ReqEn- EgressBlck-
          	Capabilities: [100 v1] Access Control Services
          		ACSCtl:	SrcValid+ TransBlk- ReqRedir+ CmpltRedir+ UpstreamFwd+ EgressCtrl- DirectTrans-

          The line explainer on the lspci page opens every line of this root port and of the switch's upstream port. Each feature in the table needs the right bits not just on the card but on every port above it:

          FeatureEvery port on the path mustLinux checks in
          Any DMAhave BusMaster+, to forward requests from belowpci_enable_bridge()
          AtomicOps to RAMroot port: complete the sizes asked for; switch ports: route them (Routing+); upstream ports: not block them (EgressBlck-)pci_enable_atomic_ops_to_root()
          PASIDcarry End-End TLP Prefixes (EETLPPrefix+); root and downstream ports: ACS ReqRedir+ and UpstreamFwd+pci_configure_eetlp_prefix(), pci_enable_pasid()
          Max payload sizeaccept packets as large as the card sends (MaxPayload in DevCtl)pcie_bus_configure_settings(), pci=pcie_bus_safe

          The path lab

          Change what the root port and the switch support. Both checks run as Linux runs them, from the same registers lspci prints:

            What lspci prints

            
                    

            AtomicOps and PASID depend on different parts: turn off prefix support and AtomicOps still work; block AtomicOps and PASID still works. A missing ACS capability does more than stop PASID: without it the switch may pass requests between its own ports, past the IOMMU. Linux usually puts the devices below it in one IOMMU group, but its grouping looks at the path above each device, not at its neighbours, so separate group numbers alone do not prove they are isolated (the known gap).

            A real root port: a reader's N100

            A reader sent the lspci of an Intel N100 mini PC. It lists five PCI bridge entries, all root ports (00:1c.0, 1c.2, 1c.3, 1c.6, 1d.0), and five devices on buses 01 to 05: a USB controller, two Ethernet ports, Wi-Fi and an NVMe drive. No switch ports, so each card has exactly one bridge above it. The GPU is built in at 00:02.0 and has none. Root port 00:1c.0, whose bus 01 holds the Renesas USB controller, prints:

            LnkCap:	Port #2, Speed 8GT/s, Width x1, ASPM L0s L1, Exit Latency L0s <1us, L1 <16us
            LnkSta:	Speed 5GT/s, Width x1
            DevCap2: … ExtFmt- EETLPPrefix-
            	 AtomicOpsCap: Routing- 32bit- 64bit- 128bitCAS-
            ACSCtl:	SrcValid+ TransBlk- ReqRedir+ CmpltRedir+ UpstreamFwd+ EgressCtrl- DirectTrans-

            Read it with the table above:

            The whole output, and the same machine's GPU, are in the line explainer.

            AtomicOps: FetchAdd, Swap and CAS

            An ordinary update of memory is two requests: read the value, then write the new one. Between them, someone else can write, and one of the updates is lost (the Lost update experiment above shows it). An AtomicOp is one request: the completer, the root complex for RAM, reads, changes and writes the value with nothing in between, and sends the old value back.

            OperationSizesMemory becomesReturns
            FetchAdd32, 64-bitold + operand (the carry out is dropped)old value
            Swap32, 64-bitoperandold value
            CAS (compare and swap)32, 64, 128-bitswap operand, only if old = compare operandold value

            The address must be a multiple of the operand size (natural alignment). These three are enough to build counters, queues and locks that a device and the CPU share, which is what GPU compute stacks use them for. Try them on 32 bytes of RAM; numbers are hexadecimal, as in setpci, and memory is little-endian:

            
                    

              Try a 64-bit FetchAdd at address 4: it is refused, because 4 is not a multiple of 8. A CAS whose compare value is wrong returns the current value and writes nothing; software reads that value and tries again.

              Turning AtomicOps on, and checking your machine

              Before a card may send AtomicOps to RAM, all of these must hold:

              1. The card's driver knows how to use them. There is no "can request" bit; the driver asks.
              2. The root port completes the sizes asked for: AtomicOpsCap: 32bit+ 64bit+ in its DevCap2. amdgpu asks for both.
              3. Every switch port in between says Routing+, and no upstream port says EgressBlck+.
              4. The card is a physical function: a VF uses its PF's setting.
              5. The driver calls pci_enable_atomic_ops_to_root(), which checks 2 to 4 and then sets AtomicOpsCtl: ReqEn+ on the card.

              On a real machine, read each part:

              # which bridges sit above the card
              lspci -tv
              # the root port: Routing+ 32bit+ 64bit+ ?
              sudo lspci -vvv -s 00:01.1 | grep AtomicOps
              # the card: ReqEn+ once its driver asked
              sudo lspci -vvv -s 03:00.0 | grep AtomicOpsCtl
              # amdgpu: "PCIE atomic ops is not supported"
              sudo dmesg | grep -i atomic

              When the check fails, amdgpu says so, and ROCm's compute driver (KFD) skips GPUs that need AtomicOps, logging PCI rejects atomics. The usual cause is a root port or a riser's switch that does not support them, which is why the same GPU can work in one slot and fail in the next.

              setpci -s 03:00.0 CAP_EXP+28.w=40:40 would set ReqEn by hand, but it skips the checks, not the hardware: AtomicOps the path cannot carry are still refused, as errors. PASID is the same: the PASID bench can turn PASIDCtl on, but behind a switch without prefix support, requests that carry one are rejected.

              Sources