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 / CXL

What if a PCIe card could be memory, not just a device?

A PCIe device is a workshop down the road. To use it, the CPU writes an order (a descriptor), the device fetches what it needs by DMA, does the work and sends a note back (an interrupt). That suits a disk or a network card. It does not suit memory: nobody wants to file an order to read one variable. CXL (Compute Express Link) turns the workshop into an extra room of the house. The CPU reaches into it with ordinary loads and stores, and a device can keep its own copies of the house's things, with the host making sure every copy stays current.

CXL does this over the same slots, wires and physical layer as PCIe. It is how servers add memory beyond their DIMM slots, share memory between machines, and let accelerators work on the same data as the CPU. The simple guide comes first; the experiments and the Linux sections replay a real QEMU machine with two CXL memory cards, for people who build or run these systems.

What claims does this page make? 6 points
  • Spec rule: protocols, messages, negotiation and flits come from the CXL specifications (the 1.0 specification is public; later ones are cited by section, not hosted) and from a survey by their authors, cited below.
  • Emulated observation: the region, the interleave, the backing files and the guest refusal come from QEMU 8.2's emulated CXL devices under Ubuntu's Linux 6.8; the raw captures, versions and commands are in the site's repository under docs/evidence/pcie-qemu. No number from them is a speed or latency.
  • Simulation: the link picker and the experiments are models that replay that machine.
  • Estimate: latencies are the cited authors' estimates.
  • Real hardware: none; this page used no CXL card.
  • Linux behaviour is quoted from its source, read on 2 October 2026.

The simple guide, in five ideas

A CPU with its own DRAM on the left. On the right, through a PCIe slot running CXL, a memory card adds more DRAM. Loads and stores travel to both the same way; the card's memory is slower but needs no driver call. CPU, cacheshome agent DRAMits own channels CXL cardin a PCIe slot More DRAMon the card load / store, 64 bytes
  1. CXL is PCIe plus two protocols. CXL.io is PCIe itself (configuration, BARs, DMA, interrupts); every CXL device has it. CXL.mem lets the CPU load and store to the device's memory. CXL.cache lets the device cache the host's memory.
  2. The link decides at power-on. A CXL card in a CXL slot trains as CXL; in a plain PCIe slot it trains as PCIe, and its CXL abilities stay off.
  3. Three device types. Type 1: an accelerator with a cache (io + cache). Type 2: an accelerator with its own memory too (io + cache + mem). Type 3: a memory expander (io + mem), by far the most common today.
  4. The host keeps everything coherent. Every copy of a 64-byte line, in the CPU or in a device, is tracked by the host. A device never has to understand the CPU's own coherence protocol.
  5. To software, CXL memory is memory. Linux shows a memory card as a NUMA node with no CPUs: slower than local DRAM, much faster than a disk, used by programs with no changes.

Protocols and device types: pick a device

Choose a device and whether the slot's root port speaks CXL. The link offers, answers and settles in its training sets, then the protocols share the link in 68-byte flits, each tagged with a two-byte protocol ID: CXL.cache and CXL.mem share one ID because they share one link layer.

Device

      On real hardware lspci shows the result in the Flex Bus port DVSEC. From the QEMU memory card on this page: FBCap: Cache- IO+ Mem+ 68BFlit+ (what it can do) and FBSta: Cache- IO+ Mem+ 68BFlit+ (what the link negotiated).

      What CXL is for, and what it costs

      The price is latency. Estimates from the specification's authors and from Pond, not measurements:

      PathEstimateSource
      Two CXL ports and the flight between them, added to a memory accessabout 57 nsDas Sharma et al.
      CPU to memory on a directly attached CXL cardabout 170 nsDas Sharma et al.
      The same through one CXL switchabout 250 nsDas Sharma et al.
      A pool shared by 8 to 16 sockets, over local DRAM+70 to 90 nsPond
      Rack-scale poolsmore than +180 nsPond

      Measured hardware varies more than the link does. On real Intel Sapphire Rapids systems, Sun et al. found the best of three CXL cards only 35% slower than memory on the other socket, and the others about two and three times slower: the memory controller on the card matters more than CXL itself. Measure your device; do not assume a remote NUMA node behaves like it.

      Versions

      1. 1.0, March 2019Intel's donated specification, on PCIe 5.0 (32 GT/s); the three protocols, 68-byte flits. 1.1 (mid-2019) added compliance testing.
      2. 2.0, 10 November 2020Switches (one level), memory pooling (a multi-logical device splits into up to 16), persistent memory, hot-plug, link encryption (IDE).
      3. 3.0, 2 August 202264 GT/s on PCIe 6.0 with 256-byte flits, multi-level switches and fabrics, memory sharing between hosts, back-invalidate coherence, peer-to-peer.
      4. 3.1, 14 November 2023Fabric routing details, host-to-host memory (GIM), TEE Security Protocol (TSP) for confidential computing.
      5. 3.2, 3 December 2024A hot-page monitoring unit for tiering, online firmware activation, more security and RAS.
      6. 4.0, 18 November 2025128 GT/s on PCIe 7.0, native x2 links, bundled ports (several links as one), up to four retimers.

      Each version keeps working with devices of the earlier ones.

      The host: CPU cores and caches, the home agent, and the host's DRAM. Below the home agent, a CXL root port, and below it the device, with its cache on one side and its memory (HDM) on the other. CPU coresand their caches Home agentaddress map, coherence Host DRAMits own channels CXL root porthost bridge, decoders Device cacheCXL.cache CXL deviceits controller Device memoryHDM, CXL.mem

      Experiments

      One host and one CXL device. The memory experiments replay the QEMU machine: two Type 3 cards on root ports 0c:00.0 and 0c:01.0, interleaved by Linux 6.8.

      Experiment

        The setup

          Messages and commands

            Notes

              For professionals: flits and channels

              FlitLayoutUsed
              68 bytesA 2-byte protocol ID, then 66 bytes. For CXL.cache and CXL.mem they are four 16-byte slots (a header slot and three for headers or data, or four of data in an all-data flit) and a 2-byte CRC (without the ID it is the 528-bit link-layer flit; a 64-byte cache line takes four data slots). For CXL.io, 64 bytes carry PCIe packets, which have their own CRCs, and the last 2 bytes are unused (CXL 3.0 section 6.2.2).CXL 1.x and 2.0, up to 32 GT/s. Every device must support it, except that CXL 4.0 lets the extra, streamlined ports of a bundled port skip it; one port of each bundle must still support it.
              256 bytesNot PCIe's 236 + 6 byte layout: a 2-byte flit header (flit type, sequence number, replay), 240 bytes of flit data, then PCIe 6.0's 8-byte CRC and 6-byte FEC. CXL.io fills the data with 236 bytes of packets and a 4-byte DLLP payload; CXL.cache and CXL.mem with their own slots.CXL 3.x and 4.0; required for 64 GT/s and up
              256 bytes, latency-optimizedtwo 128-byte halves, each with its own 6-byte CRC, so the first half can be used before the second arrivesoptional; saves about 2 to 5 ns

              Protocol IDs in a 68-byte flit: FFFF CXL.io, 5555 CXL.cache and CXL.mem, CCCC an ARB/MUX link-management packet, 9999 an idle (NULL) flit. The ARB/MUX layer decides which link layer gets the next flit. A 256-byte flit has no protocol ID: the 2-bit flit type in its header says CXL.io, cache and mem, ARB/MUX, or idle. The physical layer chapter's flit lab shows PCIe's own 256-byte layout; its CRC and FEC are the ones CXL keeps (CXL 3.0 section 6.2.3.1).

              ProtocolDevice to hostHost to device
              CXL.cacheD2H Req (RdShared, RdOwn, …), D2H Resp (snoop answers), D2H DataH2D Req (snoops: SnpData, SnpInv, SnpCur), H2D Resp (GO with the line's state, WritePull), H2D Data
              CXL.memS2M NDR (completion, no data), S2M DRS (data); since 3.0 S2M BISnpM2S Req (MemRd, …), M2S RwD (request with data: writes); since 3.0 M2S BIRsp

              Interleaving: which card holds an address?

              A region spread over several cards is striped like RAID 0: the first granularity bytes on device 0, the next on device 1, and so on. Legal settings are 1, 2, 3, 4, 6, 8, 12 or 16 ways and 256 bytes to 16 KiB. Linux computes the position as (offset ÷ granularity) mod ways (cxl_calculate_position()). Try offset 1000 with 2 ways of 4 KiB: device 1, offset 0 into its part of the region, exactly where the QEMU machine put the value written at 490001000. That offset is the device address (DPA) only when the device's part starts at DPA 0, as on that machine; a device with memory set aside before the region adds that start.

                Every decoder on the path does its share: the CEDT's window can interleave across host bridges, the host bridge across its root ports, a switch across its ports; each card's decoder then removes the position bits to get an offset into its part of the region, and adds where that part starts to get its own address (DPA, device physical address). 3, 6 and 12 ways need hardware that reports them.

                Linux, step by step

                1. Firmware describes the platform in ACPI's CEDT (CXL Early Discovery Table): a CHBS entry for each CXL host bridge (where its registers are) and a CFMWS entry for each fixed memory window (base, size, interleave across host bridges, what kinds of memory it may hold). In the QEMU boot: ACPI: CEDT … and pci0000:0c: host supports CXL.
                2. cxl_acpi turns each window into a root decoder (decoder0.0: 490000000, 4 GiB) under root0.
                3. cxl_pci binds each card by its class (0502, prog-if 10), finds its registers through the Register Locator DVSEC and makes mem0, mem1. cxl_mem and cxl_port link each card into the port tree: port1 (the host bridge) and endpoint2, endpoint3, each with decoders. The numbers follow the order the cards finished probing, which changes from boot to boot (in the captured boot mem1 is the card at 0d:00.0), so scripts pick cards by PCI address.
                4. A region is created on a root decoder, given ways, granularity, size and its target cards, and committed: Linux programs every decoder on the path.
                5. dax_cxl and kmem turn the region into a DAX device and the DAX device into memory blocks on a new NUMA node.

                The QEMU machine (QEMU 8.2; a kernel with the CXL modules and an initramfs added):

                qemu-system-x86_64 -M q35,cxl=on -m 2G,maxmem=8G,slots=8 -smp 2 -cpu max,-hypervisor \
                 -object memory-backend-file,id=vmem0,share=on,mem-path=vmem0.bin,size=256M \
                 -object memory-backend-file,id=vmem1,share=on,mem-path=vmem1.bin,size=256M \
                 -device pxb-cxl,bus_nr=12,bus=pcie.0,id=cxl.1 \
                 -device cxl-rp,port=0,bus=cxl.1,id=rp13,chassis=0,slot=2 \
                 -device cxl-rp,port=1,bus=cxl.1,id=rp14,chassis=0,slot=3 \
                 -device cxl-type3,bus=rp13,volatile-memdev=vmem0,id=cxl-vmem0 \
                 -device cxl-type3,bus=rp14,volatile-memdev=vmem1,id=cxl-vmem1 \
                 -M cxl-fmw.0.targets.0=cxl.1,cxl-fmw.0.size=4G,cxl-fmw.0.interleave-granularity=4k

                Then, with cxl-cli and daxctl from ndctl (the boot on this page wrote the same values to sysfs by hand; the order of the cards sets their positions):

                cxl list -M
                cxl create-region -m -t ram -d decoder0.0 -w 2 -g 4096 mem1 mem0
                daxctl reconfigure-device --mode=system-ram dax0.0
                numactl -H

                What came out, from sysfs in the guest:

                region0: resource 0x490000000 size 0x20000000 ways 2 gran 4096 target0 decoder2.0 target1 decoder3.0
                decoder1.0: start 0x490000000 size 0x20000000 ways 2 gran 4096 targets 0,1
                dax0.0 size 536870912 target_node 1
                node1: cpus [] MemTotal: 524288 kB

                The test that matters: six values written 4 KiB apart from 490000000 landed alternately in the two cards' backing files, offsets 0, 1000, 2000 in each. The card's own view, from lspci (press a line):

                Nothing selected yet.

                Switches, pools, sharing and security

                Sources