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
- 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.
- 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.
- 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.
- 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.
- 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.
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
- More memory, and more bandwidth, per CPU. DIMM slots run out, and each DDR channel costs hundreds of processor pins. A CXL x8 link carries a memory card's traffic over far fewer pins; a PCIe 5.0 x16 link carries about 64 GB/s each way. Shipping Type 3 cards (Micron CZ120, Samsung CMM-D, SK hynix CMM-DDR5) put 96 to 256 GB of DDR on an x8 link.
- Less stranded memory. Microsoft's Pond study found up to 25% of a cloud server's DRAM sits unused because CPUs ran out first; pooling memory across 16 sockets cut DRAM cost by about 7% with most workloads within 1-5% of local performance.
- Tiering. Hot pages in local DRAM, cold ones on CXL: Meta's TPP, partly merged in Linux 5.18, kept its workloads within 1% of all-local memory on early CXL hardware.
- Accelerators that share data. With CXL.cache a device works on host memory in its own cache, without copying buffers back and forth.
The price is latency. Estimates from the specification's authors and from Pond, not measurements:
| Path | Estimate | Source |
|---|---|---|
| Two CXL ports and the flight between them, added to a memory access | about 57 ns | Das Sharma et al. |
| CPU to memory on a directly attached CXL card | about 170 ns | Das Sharma et al. |
| The same through one CXL switch | about 250 ns | Das Sharma et al. |
| A pool shared by 8 to 16 sockets, over local DRAM | +70 to 90 ns | Pond |
| Rack-scale pools | more than +180 ns | Pond |
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.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.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.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.
- 3.1, 14 November 2023Fabric routing details, host-to-host memory (GIM), TEE Security Protocol (TSP) for confidential computing.
- 3.2, 3 December 2024A hot-page monitoring unit for tiering, online firmware activation, more security and RAS.
- 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.
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
This "What if" step ran on its own copy; the panels show that copy.
For professionals: flits and channels
| Flit | Layout | Used |
|---|---|---|
| 68 bytes | A 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 bytes | Not 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-optimized | two 128-byte halves, each with its own 6-byte CRC, so the first half can be used before the second arrives | optional; 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).
| Protocol | Device to host | Host to device |
|---|---|---|
| CXL.cache | D2H Req (RdShared, RdOwn, …), D2H Resp (snoop answers), D2H Data | H2D Req (snoops: SnpData, SnpInv, SnpCur), H2D Resp (GO with the line's state, WritePull), H2D Data |
| CXL.mem | S2M NDR (completion, no data), S2M DRS (data); since 3.0 S2M BISnp | M2S Req (MemRd, …), M2S RwD (request with data: writes); since 3.0 M2S BIRsp |
- Who is in charge of coherence. For CXL.cache the host is the home agent and the device a cache it tracks (MESI, 64-byte lines, host physical addresses, so the device translates first with ATS). For CXL.mem the host is master, the device subordinate.
- Device memory and its coherence. HDM (host-managed device memory) comes in three kinds: HDM-H, which only the host caches (Type 3); HDM-D, where a Type 2 device switches lines between host bias and device bias over CXL.cache (CXL 1.x and 2.0); and HDM-DB, where the device sends the host back-invalidate snoops (BISnp) to take lines back (CXL 3.0, an alternative to bias).
- When CXL turns on. CXL.cache and CXL.mem are enabled only once the link reaches L0 at 8 GT/s or more; a CXL link that cannot may fail to come up at all, rather than run slowly.
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
- 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 …andpci0000:0c: host supports CXL. - cxl_acpi turns each window into a root decoder (
decoder0.0:490000000, 4 GiB) underroot0. - cxl_pci binds each card by its class (
0502, prog-if10), finds its registers through the Register Locator DVSEC and makesmem0,mem1. cxl_mem and cxl_port link each card into the port tree:port1(the host bridge) andendpoint2,endpoint3, each with decoders. The numbers follow the order the cards finished probing, which changes from boot to boot (in the captured bootmem1is the card at0d:00.0), so scripts pick cards by PCI address. - 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.
- 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.
- On x86, Linux 6.8 will not commit a region when the CPU reports a hypervisor: it must invalidate CPU caches before memory appears at a new address and does not trust a guest to (
cxl region0: Failed to synchronize CPU cache state; a kernel built withCONFIG_CXL_REGION_INVALIDATION_TESTskips the check, for testing only). That is why the QEMU command has-cpu max,-hypervisor; the fourth experiment shows the failure. - With
/sys/kernel/mm/numa/demotion_enabledset, reclaim moves cold pages to the CXL node instead of swapping them out. - The enumeration chapter covers how the cards were found as PCI devices before any of this.
Switches, pools, sharing and security
- Pooling (2.0): a switch, or a card with several host ports, gives each host its own share. A multi-logical device (MLD) splits into up to 16 logical devices; a fabric manager (software in a host, the switch or a BMC) decides who gets which.
- Sharing (3.0): the same memory visible to several hosts at once, kept coherent by back-invalidate. Pooling moves memory between owners; sharing has many.
- Fabrics (3.0): several switch levels and non-tree topologies with port-based routing, up to 4096 nodes. Dynamic capacity devices hand out memory in extents at run time.
- Security: IDE (2.0) encrypts and authenticates the link with AES-GCM and a 256-bit key; TSP (3.1) lets a confidential VM trust memory on a CXL card. For how the IOMMU fits around devices, see the secure DMA chapter.
Sources
- CXL Consortium: the CXL 1.0 specification (public; sections 3.2 and 3.3 channels, 4.2 flits, 6.2.2 protocol IDs, 6.3.1.1 negotiation), the 3.0 specification's evaluation copy (section 6.4.1.1), the 2.0, 3.0 and 4.0 white papers and the 3.0 to 4.0 release announcements. Later specifications are cited, not hosted.
- D. Das Sharma, R. Blankenship and D. Berger, "An Introduction to the Compute Express Link (CXL) Interconnect", ACM Computing Surveys, 2024: protocols, coherence, flits, latency estimates.
- H. Li et al., "Pond: CXL-Based Memory Pooling Systems for Cloud Platforms", ASPLOS 2023. Y. Sun et al., "Demystifying CXL Memory with Genuine CXL-Ready Systems and Devices", MICRO 2023. H. Al Maruf et al., "TPP: Transparent Page Placement for CXL-Enabled Tiered-Memory", ASPLOS 2023. Product figures from Micron, Samsung and SK hynix announcements.
- Linux, read on 2 October 2026: Documentation/driver-api/cxl,
drivers/cxl/acpi.c,drivers/cxl/pci.c,drivers/cxl/cxl.h(eiw_to_ways,eig_to_granularity),drivers/cxl/core/region.c(cxl_calculate_position,cxl_calculate_dpa_offset,cxl_calculate_hpa_offset),include/uapi/linux/pci_regs.h(CXL DVSEC IDs); ndctl'scxl-create-regionanddaxctl-reconfigure-devicemanuals. - The machine: QEMU 8.2.2 and Ubuntu 24.04's Linux 6.8.0, run on 2 October 2026; lspci 3.10.
tests/iommu-pcie-cxl.test.mjschecks the interleave maths against that run.