Tuning AMD LBR window reach¶
The AMD LBR backend (ASMTEST_HWTRACE_AMD_LBR — see
Hardware tracing) reconstructs a routine’s exact
instruction and block stream from the CPU’s LBR branch stack. That stack
is 16 entries deep on every shipping Zen part — a silicon ceiling, not a
software choice — so how much of a run one capture can reach is a real tuning
question. This page explains what bounds a window, the knobs
asmtest_hwtrace_options_t exposes to stretch it
(asmtest_hwtrace.h), how the backend
reports the runs it could not hold, and when a statistical survey is the
better tool than a longer exact window.
Every lever here is fidelity-neutral by construction: a mis-tuned knob
degrades to a faithful truncated trace (or simply forgoes the stretch), never
to silently wrong offsets.
What bounds a window¶
The branch stack records the last 16 taken branches, and the kernel delivers it only inside a perf sample — the backend arms a branch-retired counter so a PMI snapshots the stack (see perf_event). Three consequences bound what one capture can hold:
The 16-deep window (Tier A). A routine whose whole path takes ≤ 16 branches fits one window and reconstructs exact and complete. (The depth is read from CPUID at runtime rather than assumed; every shipping part reports 16.)
The data ring and PMI throttling (Tier B). Past 16 branches, Tier-B stitching splices the overlapping windows into one gapless sequence — so the 16-branch ceiling itself is lifted, and the remaining ceiling is how many
sample_period=1windows the perf data ring can hold before the kernel drops the newest ones, plus the kernel’s sample-rate throttling (kernel.perf_event_max_sample_rate).Too-fast single-shot routines. The stack arrives only at a PMI, so a tiny routine that returns before any sample fires in-region is never captured at all. The backend reports that faithfully as
truncated; the deterministic boundary snapshot below exists for exactly this case.
How truncation is reported¶
Every incomplete capture collapses onto the one loss bit every backend shares:
asmtest_trace_t.truncated (see Execution traces). The AMD
backend sets it when a single window overflowed the 16-deep stack and could not
be stitched further, when the stitch found a gap (insufficient overlap
between consecutive windows), when the kernel signalled drops
(PERF_RECORD_LOST / throttling), or when no in-region sample fired at all. A
partial capture is never emitted as complete.
The recovery idiom is dynamic fallback: re-resolve under
ASMTEST_HWTRACE_CEILING_FREE — which skips the one backend with a bounded
completeness ceiling — and re-run on the backend that comes back (typically
single-step, which has no ceiling). See
Auto-selecting a backend, or
asmtest_trace_call_auto for the cross-tier form that escalates automatically.
The levers¶
Lever ( |
What it does |
Reach effect |
Trade |
|---|---|---|---|
|
grows the perf data ring holding the sampled windows (AMD default 256 KB) |
longer gapless Tier-B stitch before drops |
memory only |
|
a PMI every N taken branches instead of every one |
~N× fewer interrupts → less throttling/ring pressure |
undercounts self-similar loops — see below |
|
drops direct unconditional |
measured 1.86× more instructions per window |
none (byte-identical; falls back if perf rejects it) |
|
deterministic boundary snapshot at the region exit instead of sampling |
completes the too-fast tiny routine |
single-exit regions; needs the BPF substrate |
Ring size and runner sysctls — data_size¶
The Tier-B stitched capture is bounded by the data ring: a long
sample_period=1 run emits more windows than the ring holds, the kernel drops
the newest samples, and the trace faithfully truncates. Raise data_size
(bytes, rounded up to power-of-two pages; 0 selects the AMD default of
256 KB) to extend reach. On a dedicated runner, also raise
kernel.perf_event_max_sample_rate and set
kernel.perf_cpu_time_max_percent=0 so sustained per-branch sampling is not
throttled. This lever changes nothing about fidelity — only how much of the run
fits.
Fewer interrupts — lbr_period (period-spaced stitching)¶
lbr_period = N (opt-in; 0 keeps the exact default) takes a PMI every N
taken branches instead of every one, so consecutive 16-deep windows overlap by
16 − N entries and still stitch — at ~N× fewer interrupts, cutting the
throttling and ring pressure that end a long capture. N must stay below the
LBR depth (the backend clamps it) so an overlap always remains; an over-large
value just produces a stitch gap and a faithful truncated.
The caveat that keeps the default at 0: a loop’s taken edges repeat
identically every iteration, which gives the stitcher no way to tell 1 skipped
iteration from N — so lbr_period > 1 silently undercounts self-similar
loops. This is not theoretical: measured live on the Zen 5 dev box, the
16-deep-overflowing loop fixture reconstructs fewer instructions at period 4
than at period 1 (231 vs 297). Since every loop is edge-self-similar, the
lever’s reach gain is confined to distinct-edge straight-line paths — which
are inherently short. Prefer branch_filter for loops; reserve lbr_period
for long branchy non-looping paths.
Stretching each window — branch_filter (reduced filter)¶
A direct unconditional jmp has a statically decodable target, so recording it
wastes one of the 16 scarce LBR slots. branch_filter = 1 (opt-in; 0 keeps
PERF_SAMPLE_BRANCH_ANY) asks the hardware to record only
conditional / indirect-jump / call / return branches; the reconstructor follows
the dropped jmps from the registered region bytes itself, so the trace stays
byte-identical — the knob changes capture efficiency, never fidelity.
Because each window now spends its slots only on branches the decoder cannot
infer, one window spans more of the routine: measured on the Zen 5 dev box, a
loop whose body carries a direct jmp plus a conditional back-edge
reconstructs 1.86× more instructions per 16-deep window (65 vs 35). This is
the window-stretch lever that does help loops. It applies to the sampled and
deterministic-snapshot exact paths (not the statistical survey), and if perf
rejects the filter combination the capture silently retries with the full
filter — the tier stays available, merely forgoing the stretch.
The deterministic boundary snapshot — snapshot¶
On the AMD backend, snapshot opts the begin/end markers into a
deterministic boundary capture: a hardware breakpoint at the region’s exit
triggers a small eBPF program that reads the frozen 16-entry stack at that one
point (bpf_get_branch_snapshot), instead of hoping a sample_period=1 PMI
lands in-region. This is the fix for the too-fast single-shot routine — the
case the sampled path can only mark truncated. Where the substrate supports
it, the backend already selects the snapshot by default for single-exit
regions; any arm failure (no BPF toolchain, missing capabilities, no
LbrExtV2) falls back cleanly to the sampled path. Note the snapshot is one
frozen window: it completes small routines, it does not stitch past 16
branches.
Privileged vs unprivileged lanes¶
The reach levers sit on top of a privilege gradient. What actually runs where:
Path |
Needs |
Containerized lane |
|---|---|---|
Sampled LBR (+ |
Zen 4+ LbrExtV2 silicon (Zen 3 BRS not yet openable); |
|
Deterministic snapshot |
the above plus |
|
MSR-direct snapshot |
root / |
|
single-step fallback |
nothing — no PMU, no perf, no privilege |
|
Statistical IBS survey |
any Zen incl. Zen 2; unprivileged at the default |
|
asmtest_hwtrace_available(ASMTEST_HWTRACE_AMD_LBR) collapses the silicon +
kernel + privilege chain into one predicate, and
asmtest_hwtrace_skip_reason() says which gate failed — so a suite self-skips
with the reason (e.g. perf denied vs. no branch-record hardware) instead of
failing. On a host where you control the knob,
sudo sysctl kernel.perf_event_paranoid=1 (or granting CAP_PERFMON) is what
opens the sampled lane; no amount of tuning opens it from inside an
unprivileged process.
When the statistical IBS lane is the better tool¶
Window tuning buys reach for an exact trace of one registered region. Reaching for it is the wrong move when:
the host is Zen 2 — there is no branch stack at all; every exact AMD hardware path self-skips, and IBS is the only hardware branch source;
you cannot get perf privilege — the IBS lane runs user-only and unprivileged at the default
perf_event_paranoid=2;the target is a live process / JIT runtime you must not perturb — IBS attaches out of band to any same-uid pid at full speed, where an exact window needs the begin/end markers in-process;
the question is “where is it hot?”, not “exactly what ran?” — a hot-edge histogram over a longer window answers it without any ceiling to tune.
The lane is candid about being statistical: it fills its own survey shape,
never asmtest_trace_t, and a sampled edge proves presence but absence proves
nothing. See
the IBS-Op lane
and the interactive asmspy --sample view. The AMD whole-window
statistical survey falls back to IBS on branch-stack-less hosts automatically.
See also¶
Hardware tracing — the backend table, region lifecycle, and auto-selection this page tunes.
Execution traces — the shared
asmtest_trace_tshape and thetruncatedcontract.Native runtime tracing — the ceiling-free DynamoRIO and out-of-process stepper tiers a truncated capture escalates to.
asmtest_hwtrace.h — the
asmtest_hwtrace_options_tfield documentation these levers ship with.The window-size lever design notes (internal): amd-tracing-plan.md (Matrix 2, “Window-size levers — status”).