[llvm] [llvm-exegesis] Fix LBR hang on hybrid x86 CPUs (PR #216917)

Fangrui Song via llvm-commits llvm-commits at lists.llvm.org
Mon Aug 17 22:16:31 PDT 2026


https://github.com/MaskRay created https://github.com/llvm/llvm-project/pull/216917

Hybrid CPUs (Alder Lake and newer) register one PMU per core type: cpu_core
for the P-cores and cpu_atom for the E-cores. X86LbrPerfEvent programs a
P-core encoding of BR_INST_RETIRED.NEAR_TAKEN and cpu_core claims
PERF_TYPE_RAW, so the kernel never schedules the event while we run on an
E-core. perf_event_open succeeds anyway and the event records nothing, so
doReadCounter polled for 160*10s (~27 minutes). This stalls a full
llvm/test run built with LLVM_ENABLE_LIBPFM=ON, because lit.local.cfg
probes llvm-exegesis during test discovery, before any test runs.

Pin to the cpu_core CPU set, intersected with the current affinity mask so
that taskset still wins, and report an error when the intersection is
empty. Cut the poll budget to ~3s as well; the event is disabled before
polling, so a sample is either already in the ring buffer or will never
arrive. Give the lit probe a timeout too.

The non-LBR counters share this root cause but already fail loudly via
PerfCounterNotFullyEnabled, so pinning them is left to a follow-up.

Aided by Claude Opus 5


>From 5e8cbf0aa45b3f720e22171a73ad63e7ad046a4f Mon Sep 17 00:00:00 2001
From: Fangrui Song <i at maskray.me>
Date: Mon, 17 Aug 2026 21:48:11 -0700
Subject: [PATCH] [llvm-exegesis] Fix LBR hang on hybrid x86 CPUs

Hybrid CPUs (Alder Lake and newer) register one PMU per core type: cpu_core
for the P-cores and cpu_atom for the E-cores. X86LbrPerfEvent programs a
P-core encoding of BR_INST_RETIRED.NEAR_TAKEN and cpu_core claims
PERF_TYPE_RAW, so the kernel never schedules the event while we run on an
E-core. perf_event_open succeeds anyway and the event records nothing, so
doReadCounter polled for 160*10s (~27 minutes). This stalls a full
llvm/test run built with LLVM_ENABLE_LIBPFM=ON, because lit.local.cfg
probes llvm-exegesis during test discovery, before any test runs.

Pin to the cpu_core CPU set, intersected with the current affinity mask so
that taskset still wins, and report an error when the intersection is
empty. Cut the poll budget to ~3s as well; the event is disabled before
polling, so a sample is either already in the ring buffer or will never
arrive. Give the lit probe a timeout too.

The non-LBR counters share this root cause but already fail loudly via
PerfCounterNotFullyEnabled, so pinning them is left to a follow-up.

Aided by Claude Opus 5
---
 llvm/test/tools/llvm-exegesis/lit.local.cfg   |  7 ++
 .../llvm-exegesis/lib/X86/X86Counter.cpp      | 81 +++++++++++++++++--
 llvm/tools/llvm-exegesis/lib/X86/X86Counter.h |  3 +
 3 files changed, 86 insertions(+), 5 deletions(-)

diff --git a/llvm/test/tools/llvm-exegesis/lit.local.cfg b/llvm/test/tools/llvm-exegesis/lit.local.cfg
index 89110ed2816cd..4951e587e2f17 100644
--- a/llvm/test/tools/llvm-exegesis/lit.local.cfg
+++ b/llvm/test/tools/llvm-exegesis/lit.local.cfg
@@ -19,13 +19,20 @@ def can_use_perf_counters(mode, extra_options=[]):
         print("could not find llvm-exegesis")
         return False
     try:
+        # This runs during test discovery, so a hung llvm-exegesis would stall
+        # the whole lit invocation before a single test starts. Treat a probe
+        # that takes this long as a probe that failed.
         return_code = subprocess.call(
             [llvm_exegesis_exe, "-mode", mode, "-opcode-name=ADD64rr"]
             + extra_options,
             stdout=subprocess.DEVNULL,
             stderr=subprocess.DEVNULL,
+            timeout=60,
         )
         return return_code == 0
+    except subprocess.TimeoutExpired:
+        print("llvm-exegesis timed out")
+        return False
     except OSError:
         print("could not exec llvm-exegesis")
         return False
diff --git a/llvm/tools/llvm-exegesis/lib/X86/X86Counter.cpp b/llvm/tools/llvm-exegesis/lib/X86/X86Counter.cpp
index 9dc6c764599cd..24327790437c9 100644
--- a/llvm/tools/llvm-exegesis/lib/X86/X86Counter.cpp
+++ b/llvm/tools/llvm-exegesis/lib/X86/X86Counter.cpp
@@ -14,8 +14,12 @@
 // FIXME: Use appropriate wrappers for poll.h and mman.h
 // to support Windows and remove this linux-only guard.
 
+#include "llvm/ADT/SmallVector.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/ADT/Twine.h"
 #include "llvm/Support/Endian.h"
 #include "llvm/Support/Errc.h"
+#include "llvm/Support/MemoryBuffer.h"
 
 #include <perfmon/perf_event.h>
 #include <perfmon/pfmlib.h>
@@ -29,6 +33,7 @@
 #include <memory>
 
 #include <poll.h>
+#include <sched.h>
 #include <sys/mman.h>
 #include <unistd.h>
 
@@ -44,13 +49,75 @@ static const size_t kDataBufferSize = kBufferPages * getpagesize();
 // the next page, so we allocate one more page.
 static const size_t kMappedBufferSize = (kBufferPages + 1) * getpagesize();
 
+// The event is disabled before we poll, so a sample either is already in the
+// ring buffer -- wakeup_events == 1 leaves POLLIN asserted -- or it will never
+// arrive. One poll is therefore conclusive, and the retries only absorb wake-up
+// latency. This used to be 160 * 10s, which turned a host without working LBR
+// into a 27 minute hang.
+constexpr int kPollTimeoutMs = 1000;
+constexpr int kMaxTimeouts = 2;
+
 // Waits for the LBR perf events.
 static int pollLbrPerfEvent(const int FileDescriptor) {
   struct pollfd PollFd;
   PollFd.fd = FileDescriptor;
   PollFd.events = POLLIN;
   PollFd.revents = 0;
-  return poll(&PollFd, 1 /* num of fds */, 10000 /* timeout in ms */);
+  return poll(&PollFd, 1 /* num of fds */, kPollTimeoutMs);
+}
+
+// Pins the process to the P-cores of a hybrid CPU (Alder Lake and newer).
+//
+// Such CPUs have one PMU per core type -- "cpu_core" for the P-cores and
+// "cpu_atom" for the E-cores -- rather than a single "cpu" PMU. The event below
+// is a P-core encoding and "cpu_core" claims PERF_TYPE_RAW, so the kernel never
+// schedules it while we run on an E-core. perf_event_open() succeeds there all
+// the same, so the event silently records nothing at all and we just poll until
+// we give up. Pinning also keeps a measurement from migrating mid-flight.
+static Error pinToLbrCapableCpus() {
+  // sysfs misreports the size of its files, so read this one as a stream. It
+  // only exists on a hybrid CPU; anywhere else every core can run the event.
+  auto BufferOrErr = MemoryBuffer::getFileAsStream(
+      "/sys/bus/event_source/devices/cpu_core/cpus");
+  if (!BufferOrErr)
+    return Error::success();
+  StringRef Cpus = (*BufferOrErr)->getBuffer().trim();
+
+  cpu_set_t Current;
+  if (sched_getaffinity(0, sizeof(Current), &Current) != 0)
+    return make_error<StringError>("Cannot read the CPU affinity.",
+                                   errc::io_error);
+
+  // A list of ranges, e.g. "0-15" or "0,2,4-7". Intersecting it with the
+  // affinity mask lets an affinity the user set with taskset(1) still win.
+  cpu_set_t PCores;
+  CPU_ZERO(&PCores);
+  SmallVector<StringRef, 4> Ranges;
+  Cpus.split(Ranges, ',');
+  for (StringRef Range : Ranges) {
+    auto [FirstStr, LastStr] = Range.split('-');
+    // A bare "N" is the same as the range "N-N".
+    if (LastStr.empty())
+      LastStr = FirstStr;
+    unsigned First, Last;
+    if (FirstStr.getAsInteger(10, First) || LastStr.getAsInteger(10, Last))
+      return make_error<StringError>("Cannot parse the P-core CPU list '" +
+                                         Cpus + "'",
+                                     errc::invalid_argument);
+    for (unsigned I = First; I <= Last && I < CPU_SETSIZE; ++I)
+      if (CPU_ISSET(I, &Current))
+        CPU_SET(I, &PCores);
+  }
+
+  if (CPU_COUNT(&PCores) == 0)
+    return make_error<StringError>(
+        "LBR measurements have to run on a P-core, but the CPU affinity mask "
+        "of this process selects E-cores only.",
+        errc::not_supported);
+  if (sched_setaffinity(0, sizeof(PCores), &PCores) != 0)
+    return make_error<StringError>("Cannot pin the process to the P-cores.",
+                                   errc::io_error);
+  return Error::success();
 }
 
 // Copies the data-buffer into Buf, given the pointer to MMapped.
@@ -122,7 +189,9 @@ X86LbrPerfEvent::X86LbrPerfEvent(unsigned SamplingPeriod) {
   Attr = new perf_event_attr();
   Attr->size = sizeof(*Attr);
   Attr->type = PERF_TYPE_RAW;
-  // FIXME This is SKL's encoding. Not sure if it'll change.
+  // FIXME This is SKL's encoding. Not sure if it'll change. It is also a
+  // P-core encoding, so on a hybrid CPU this only counts while running on a
+  // P-core; see pinToLbrCapableCpus().
   Attr->config = 0x20c4; // BR_INST_RETIRED.NEAR_TAKEN
   Attr->sample_type = PERF_SAMPLE_BRANCH_STACK;
   // Don't need to specify "USER" because we've already excluded HV and Kernel.
@@ -155,6 +224,11 @@ void X86LbrCounter::start() {
 }
 
 Error X86LbrCounter::checkLbrSupport() {
+  // Make sure we do not run on an E-core, where the event would silently
+  // record nothing at all.
+  if (Error E = pinToLbrCapableCpus())
+    return E;
+
   // Do a sample read and check if the results contain non-zero values.
 
   X86LbrCounter counter(X86LbrPerfEvent(123));
@@ -209,9 +283,6 @@ X86LbrCounter::readOrError(StringRef FunctionBytes) const {
 
 Expected<SmallVector<int64_t, 4>>
 X86LbrCounter::doReadCounter(const void *From, const void *To) const {
-  // The max number of time-outs/retries before we give up.
-  static constexpr int kMaxTimeouts = 160;
-
   // Parses the LBR buffer and fills CycleArray with the sequence of cycle
   // counts from the buffer.
   SmallVector<int64_t, 4> CycleArray;
diff --git a/llvm/tools/llvm-exegesis/lib/X86/X86Counter.h b/llvm/tools/llvm-exegesis/lib/X86/X86Counter.h
index 75f687d395dec..527a3e957f404 100644
--- a/llvm/tools/llvm-exegesis/lib/X86/X86Counter.h
+++ b/llvm/tools/llvm-exegesis/lib/X86/X86Counter.h
@@ -33,6 +33,9 @@ class X86LbrPerfEvent : public pfm::PerfEvent {
 
 class X86LbrCounter : public pfm::CounterGroup {
 public:
+  // Checks whether the host supports LBR with cycles. On a hybrid CPU this also
+  // pins the process to the P-cores, and that pinning persists so that the
+  // later measurement runs stay on a CPU where the event is scheduled.
   static Error checkLbrSupport();
 
   explicit X86LbrCounter(pfm::PerfEvent &&Event);



More information about the llvm-commits mailing list