Skip to content

perf(pool): size the default pool from the CPUs the process may run on - #944

Merged
singaraiona merged 5 commits into
devfrom
perf/pool-cpu-set
Oct 22, 2026
Merged

singaraiona merged 5 commits into
devfrom
perf/pool-cpu-set

Conversation

@ser-vasilich

Copy link
Copy Markdown
Collaborator

The default pool had one thread per online CPU even under taskset, a cgroup cpuset or docker --cpuset-cpus. An instance confined to 8 CPUs of a 48-CPU machine ran 48 threads on them. Under a CPU quota (docker --cpus, systemd CPUQuota=) the pool also had more threads than the quota lets run, and they were throttled in turn.

  • The default pool size is now the process affinity mask on Linux (the online CPU count elsewhere), or the cgroup CPU quota rounded up when that is smaller (v2 cpu.max, v1 cfs quota/period, smallest over the cgroup and its ancestors). Threads are not tied to CPUs.
  • A -c larger than that is obeyed, with a warning on stderr.
  • .sys.info: cores is the CPUs the process may run on; the new key workers is the pool size, main included. The banner prints the same CPU count.
  • A new guide, docs/guides/instances.md, explains how to run several instances on one machine: CPUs, memory, ports, journals, spill files and sharing a database. It also corrects stale statements about symbol files in the storage docs.
  • Tests: pool/auto_follows_cpu_set and rfl/system/pool_cpuset.rfl (child processes under taskset and a CPU quota, Linux only via the new ;; @requires: linux tag).

A guide to CPU sets, memory limits, ports, journals and spill files per
instance, and to what is safe when instances share a database: any
number of readers, one writing process per database root, no in-place
edits of table files, one rayforce version.

The storage pages said symbol-file writes were safe from several
processes and that symbol files carry checksums; neither holds. The
spill-file naming now lists direct blocks too. The REPL and pipeline
pages describe the pool size and core pinning.
The default pool had one thread per online CPU also under taskset or a
cgroup cpuset, so an instance confined to 8 CPUs ran a thread per machine
CPU on them, and under a CPU quota (docker --cpus, systemd CPUQuota=) more
threads than the quota lets run, throttled in turn.

The default size is now the process affinity mask on Linux (the online
count elsewhere), or the CPU quota rounded up when that is smaller: the
smallest v2 cpu.max or v1 cfs quota over its period on the process's
cgroup and its ancestors.  Threads are not tied to CPUs; the scheduler
places them within the mask.  A -c larger than that is obeyed with a
warning on stderr.

.sys.info reported the online CPU count; cores is now the CPUs the process
may run on, and the new key workers is the pool size, main included.  The
banner prints the same CPU count.  The RFL runner takes ";; @requires:
linux" for files that check Linux-only facilities (taskset, cgroups).
The TSan job builds only the test binary and runs the files whose name
holds "pool"; pool_cpuset started ./rayforce children, which that job
does not build.  Renamed to cpu_set_size.

@morsel-ray morsel-ray left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read the complete21-file feature diff at faf79f1 (actual merge base d7153e7, not the newly accepted014 release tree). The placement of this work is appropriate: affinity/quota reads are at initialization/explicit CLI sizing, not in row/task loops, and explicit overrides remain overrides.

Two Linux boundary-detection cases still lose the accessible limit; details inline. I independently compiled the exact platform/pool TUs and exercised controlled fopen/affinity/sysconf fixtures: canonical v1/v2→2 participants and tighter ancestor→1 work; subtree-v2/noncanonical-v1 layouts return8 instead of2, and a large-kernel-mask EINVAL falls back to2048 online instead of the permitted2. These are interposed API/file-layout controls, not a claim of a real cgroup/mount deployment, large-host run or full PR build. No pool was created for the large-mask control.

Also please make the positive quota test's prerequisites/skip visible: the systemd-user guard can return0 without executing a quota assertion, and a tighter inherited user-manager quota can legitimately make the expected total1 rather than the literal2. Green file status alone should not stand for positive quota coverage. The new .sys.info pool initialization/frozen configuration is a user-visible compatibility detail worth calling out explicitly.

No independent whole-suite/TSan/performance or cross-platform compile claim. These requests are initialization-boundary fixes, not a request for new executor validation or altered task geometry.

Comment thread src/core/platform.c Outdated
Comment thread src/core/platform.c Outdated
… masks

The quota was read under /sys/fs/cgroup at the membership path of
/proc/self/cgroup.  Where a container mounts its own v2 subtree there, or
a v1 cpu controller sits elsewhere, those paths miss the quota files and
the pool is sized to every allowed CPU again.  The hierarchy's mount point
and the subtree it shows now come from /proc/self/mountinfo; the
membership is translated through that root and its visible ancestors are
read up to the mount point.  The conventional mounts remain the fallback
without mountinfo.

On a kernel built for more CPUs than a cpu_set_t holds, sched_getaffinity
rejects the fixed buffer with EINVAL however few CPUs are allowed, and the
count fell back to every online CPU.  It now retries with a larger buffer.

.sys.info created the pool to report its size; it now reports the size a
default pool would get while none exists, from the rule the pool uses.

The quota check of a child moves to pool/cpu_quota_child, which skips
visibly without a systemd user manager or ./rayforce and expects at most
the size the same child gets in a scope without a quota.

@morsel-ray morsel-ray left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up at cf21e6e: the two original single-view quota mappings and dynamic affinity retry are repaired in my independent controls; affinity thread resolved. The remaining multiple-visible-mount selection case is detailed in the existing quota thread, which stays open.

Fresh all-TU ASan/UBSan: platform8, pool auto2, CLI cores2, cpu-set RFL1, reserved-namespace2, system-branch1 passed. Positive real-quota child is explicitly SKIP here (no systemd user manager), not a passed quota workload. GCC five changed TUs×DEBUG/NDEBUG10 warning-clean. A real CLI without -c, with ENV3, gives (1 1 4) for task-count before/after .sys.info and reported workers: introspection no longer creates the pool. Source/HEAD/both binaries and whole relevant diagnostic logs verified. No full/TSan/native-large-host/cgroup-deployment/performance claim.

One test-only portability item below: production wide-mask handling is fixed, but the new regression still snapshots affinity with a fixed buffer. Please also avoid assuming1023 is larger than every real runner's allowed/default CPU count in the warning test; an affinity-confined child or a policy-derived expected warning avoids that assumption.

Comment thread test/test_platform.c Outdated
A cgroup hierarchy can be mounted more than once (a subtree and the whole
tree, or an unrelated subtree first).  The quota reader stopped at the
first mount of the hierarchy, readable or not; it now reads every mount
whose subtree holds the membership and keeps the tightest quota visible
through any of them.

The tests snapshot the affinity mask with a fixed buffer, which a kernel
with a wider mask rejects: they now skip there.  The -c warning test
expects the warning exactly when -c passes the default pool size rather
than assuming fewer CPUs than 1023.

@morsel-ray morsel-ray left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved e76dab1 after reading the complete original feature and both revisions, and independently verifying the requested repairs. All my findings are resolved.

  • Exact-helper controls: all9 policy cases pass (canonical v1/v2, subtree/noncanonical paths, tighter ancestor, large mask-buffer retry, multiple/unreadable views and the wider visible ancestor). First-eligible-view-only counterfactual fails as intended. Synthetic API/file fixtures—not real mounts or large hardware.
  • Fresh forced all-TU nonroot Clang ASan/UBSan: platform8/8, pool auto2/2, CLI cores2/2, cpu_set_size RFL1/1, reserved namespace2/2, system branch1/1; whole relevant logs diagnostic-free. Real quota child:1 explicit SKIP, no systemd user manager here.
  • GCC six changed TUs×DEBUG/NDEBUG12 syntax checks pass. Real CLI with no -c and ENV3 reports (1 1 4) for task-count before/after .sys.info and hypothetical workers: introspection no longer creates the pool. Exact clean HEAD, all tracked sources and both binary hashes independently verified.

No independent full-suite/TSan/strict-docs, real quota deployment, worker-participation or performance claim. Please normally integrate the current accepted dev and obtain fresh required gates before landing; this approval covers the examined feature, not unreviewed future changes. Body breadcrumb: the tested RFL's actual name is rfl/system/cpu_set_size.rfl, rather than the old pool_cpuset spelling.

@singaraiona
singaraiona merged commit 62d9bff into dev Oct 22, 2026
11 checks passed
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.

3 participants