2026-07-14
lscpu tells you how many cores you have. lstopo shows you where they are — the exact hierarchy of sockets, NUMA nodes, L3/L2/L1 caches, sibling hyperthreads, and which PCIe devices sit on which NUMA node. It's part of hwloc (Portable Hardware Locality), the library that Open MPI, SLURM, and every serious process-pinning tool call internally. Package name is hwloc on Debian and RHEL.
Open a graphical map, or fall back to the terminal:
lstopo # X11 window with the full topology drawn
lstopo-no-graphics # curses-y summary in the tty
lstopo --of console # ASCII art tree
lstopo --of svg > box.svg # for the wiki page
The tree shows Package → NUMANode → L3 → L2/L1 → Core → PU. That's the actual topology; the flat "48 CPUs" list from /proc/cpuinfo hides all of it.
Where should my NIC IRQs live?
lstopo --whole-io --physical
Now PCI devices show up with their NUMA node. If your 100GbE card is on node 1 but your packet processor is pinned to node 0, every frame crosses UPI and you're eating 30–40% of your throughput in cross-socket traffic. This one command has settled dozens of "why is our tail latency weird" arguments.
Which cores share L2/L3? Useful for co-locating a producer and consumer thread:
lstopo --no-io --only core
lstopo --output-format xml # machine-parseable for schedulers
Distance matrix between NUMA nodes:
hwloc-distances
Same data the kernel uses, but readable. On modern EPYC boxes with chiplet-based CCDs, the numbers are surprising.
Pin a process by semantic location instead of hardcoded CPU IDs:
hwloc-bind L3:0 -- ./my_worker # any core sharing L3 #0
hwloc-bind NUMANode:1.Core:2 -- ./job # specific core on node 1
hwloc-bind socket:0 --membind socket:0 -- ./crunch
Convert those selectors to a mask you can hand to taskset, numactl, or systemd's CPUAffinity=:
$ hwloc-calc --taskset NUMANode:1
0xffff0000
$ systemctl set-property redis.service CPUAffinity=$(hwloc-calc NUMANode:0)
Distribute N tasks evenly across the machine's real physical cores (not siblings):
hwloc-distrib --single 8 --restrict $(hwloc-calc Package:0)
numactl --hardware knows nodes and distances. taskset takes a raw bitmask. Neither knows anything about the cache hierarchy, PCI locality, or which "CPU 24" is actually the SMT sibling of "CPU 0". hwloc gives you the whole picture in one map, and the selector syntax means the same pinning script works unchanged on your Xeon workstation and your EPYC server — because L3:0.Core:1 means the same semantic thing on both, even when the underlying CPU numbering is wildly different.
Bonus: lstopo --top gives a live view showing which PUs are currently running which processes. It's htop arranged the way your silicon actually is.
lscpu counts your cores; lstopo shows you the cache, NUMA, and PCIe map they actually live in — and hwloc-bind lets you pin by semantic location so the same script works on every machine.
