Skip to content

docs(thread-affinity): state the CPU placement axis, and what would close it - #133

Open
Helldez wants to merge 1 commit into
mainfrom
exp/thread-affinity
Open

docs(thread-affinity): state the CPU placement axis, and what would close it#133
Helldez wants to merge 1 commit into
mainfrom
exp/thread-affinity

Conversation

@Helldez

@Helldez Helldez commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Documentation only. No code, no flags, nothing implemented.

The engine picks a thread count and leaves placement to the kernel. Decode on the
reference device is compute-bound, so CPU placement is the one open axis that acts on
compute rather than on flash, and it has been carried for weeks as a one-line "cheapest
untested lever" without anyone writing down what would actually be tested. This PR writes
it down, so the experiment is not re-derived a third time.

What the note establishes

Three mechanisms hide behind one word, and they disagree about the intervention.
Barrier skew (a node costs what its slowest thread costs) and migration deflating the
governor's per-core utilisation both argue for taking the fast cores. The reader as a
co-tenant argues for the opposite: compute threads that spin on every core deschedule the
O_DIRECT workers, and that cost lands in stall, not in compute. They are
distinguishable only if the verdict metric is the decode breakdown rather than tok/s,
which is the mistake the note mainly exists to prevent.

The result cannot ship as a core mask. cgroup cpusets that the vendor controls and
that change with foreground state, vendor userspace that moves threads, topology that
varies by part (4+4, 1+3+4, 2+6, three clusters, parts with no low-power cluster), and
per-cluster frequency caps that move during a run, so a mask computed at startup from
"which cores are fastest" can describe a machine that no longer exists ninety seconds in.
What is discoverable at runtime is sched_getaffinity as the granted universe,
cpu_capacity, and related_cpus, which makes the shippable unit a policy rather than a
mask, and makes the frequency-invariant policy safer than the frequency-derived one.

Prior art is surveyed rather than assumed. The pinned llama.cpp already exposes
--cpu-mask, --cpu-strict and --poll through the public threadpool API, and
upstream's own Snapdragon bench script excludes the low-power cores. ncnn has shipped a
three-value policy enum across the Android fleet for years. MNN-AECS selects cores
adaptively at runtime. All of them assume resident weights, so the co-tenant hypothesis
is the part that is ours to measure.

The first two cells need no engine change and either one can close the axis.
Per-thread user times from /proc during a real decode settle whether skew exists at all;
an upstream llama-bench sweep settles whether this SoC has an exploitable placement
effect. Only then is there anything to build. The confounds are listed with them: the
frequency regime differs between adb and in-app and decays under load, so cells must be
long and the cap sampled during them, and concentrating threads on one cluster throttles
sooner, which can hand back a short cell's gain.

Pinning the read workers is a separate, already closed question (under 2%, measured with
bmoe-iobench). The note records it explicitly so the two results are never quoted for
each other.

Files

  • docs/thread-affinity.md, new.
  • docs/roadmap.md, a section for the axis pointing at it.
  • docs/README.md, index row.
  • CHANGELOG.md, [Unreleased] documentation entry.

The branch previously held a single empty placeholder commit from July with no PR; it has
been replaced by this one on current main.

What this changes about the July proposal

This PR replaces the original "pin the compute threads to the big cores" proposal that opened
it. Three parts of that framing did not survive being written out properly:

  • It assumed a single mechanism (barrier skew) and therefore a single direction. There are
    three, and the co-tenant one wants the opposite mask.
  • It proposed shipping --cpu-mask <hex>, which cannot be portable on Android. The surface
    becomes a runtime-resolved policy, and there are two threadpools to configure, not one,
    since prefill and decode are opposite regimes.
  • It carried an expected "+5-15%" that was an estimate presented next to measurements. No
    number is claimed here. The two zero-code cells come first, and either can close the axis.

The -t knee is kept, and promoted: it may be the whole result, and it is the one form of the
answer with none of the portability hazards.

@Helldez
Helldez force-pushed the exp/thread-affinity branch from d1ee017 to 1578d8f Compare August 17, 2026 15:17
@Helldez
Helldez marked this pull request as ready for review August 17, 2026 15:18
@Helldez Helldez changed the title feat(cli): pin compute threads to big cores (--cpu-mask) docs(thread-affinity): state the CPU placement axis, and what would close it Aug 17, 2026
…lose it

The engine picks a thread count and leaves placement to the kernel. Decode is
compute-bound, so this is the one open axis that acts on compute rather than on
flash, and it has been carried as a one-line "cheapest untested lever" for weeks
without anyone writing down what would actually be tested.

Three mechanisms hide behind the single word. Barrier skew and migration
deflating the governor's per-core utilisation both argue for taking the fast
cores; the reader as a co-tenant argues for leaving one free, since compute
threads that spin on every core push the cost into stall rather than compute.
They are distinguishable only if the verdict metric is the decode breakdown
instead of tok/s, which is the main thing this note is meant to prevent getting
wrong.

It also records why the result cannot ship as a mask: cgroup cpusets, vendor
userspace moving threads, topology that varies by part, and per-cluster caps
that move during a run. The shippable unit is a policy resolved from
sched_getaffinity, cpu_capacity and related_cpus.

Prior art is surveyed rather than assumed. llama.cpp already exposes the flags
through the public threadpool API, so the first two cells need no engine change
and either can close the axis outright. ncnn and MNN answer the portability
question the same way. None of them streams its weights, which is why the
co-tenant hypothesis is ours to measure.

Nothing is implemented. Pinning the read workers is separately already closed at
under 2%, and the note keeps the two results from being quoted for each other.
@Helldez
Helldez force-pushed the exp/thread-affinity branch from 1578d8f to bdd938c Compare August 17, 2026 15:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant