Skip to content

[Tracking]: explain the per-thread cost growth at 16 vCPUs (THREAD-HB C3) #90

Open

Description

@YUZHEthefool

Plan item: N3

Accepted parent decision

THREAD-HB (#62), option C3. The maintainer chose C2 (per-wave heartbeats, #89) and C3 in parallel on 2026-11-09. On 2026-11-10 they decided that C3 gets its own tracker, so #62 can close on its completion criterion. Design: Zero-kernel/Nilix-docs-private#5 (design/thread-hb-c3-scaling-design.md).

Target outcome

The host cost per thread of the thread stress profile at 16 vCPUs is explained. Then either a kernel repair brings it within 2× of the 4-vCPU cost on the same QEMU build, or the excess is recorded, with evidence, as an emulator limit.

Scope and non-goals

Measured with phased tracing (#88). Records: .validation/thread-hb-c1/, plus the per-wave diagnostics in #62.

vCPUs (= workers) Threads per round Host round time, QEMU 8.2 Host cost per thread
4 32 1.3 s ≈ 41 ms
8 64 4.8 s ≈ 75 ms
16 128 28.5 s ≈ 220 ms

Context switches per thread stay at about 3.3 from 8 to 16 vCPUs; their cost grows. Nearly every switch is a migration. The same image takes 17.8 s per round on QEMU 6.2 and 28.5 s on 8.2, so part of the cost is emulator cost.

Ranked hypotheses:

  1. Broadcast TLB shootdowns from the collect_target_cpus fallback (kernel/mm/tlb_shootdown.rs). Its counters exist but nothing reads them.
  2. Contention on the global PT_LOCK (kernel/mm/page_table.rs).
  3. A migration on every switch.
  4. TCG cost of cross-vCPU events.
  5. Tick debt.

Non-goals:

  • No change to the stress bound, the workload or any verdict.
  • The C2 heartbeat granularity is already decided and is not reopened here.
  • This tracker does not gate N1. The thread profile already qualifies at 16 vCPUs.

Work items

  • E1: export the shootdown counters (targeted, broadcast fallback, IPIs per request), per-CPU IPI send/receive counts, and PT_LOCK acquisition, contention and wait/hold TSC statistics in /proc/schedstat. These are relaxed atomics, kept as permanent observability. It touches MM and IPI code, so it stops for review before merge.
  • E2: diagnostic runs with workers fixed at 16 while vCPUs are 4, 8 and 16 (a diagnostic-only stress override)
  • E3: a diagnostic variant that pins each worker to one CPU
  • E4: host perf of QEMU during a 16-vCPU round. If that is unavailable, compare E1's counts across QEMU 6.2 and 8.2.
  • Decision: name the established cause and choose a repair. Each candidate repair needs its own design: skip or target shootdown IPIs, per-address-space page-table locking, or wake-affine placement. If no kernel cause is found, record the TCG limit.

Dependencies and ordering

Starts once the design (Zero-kernel/Nilix-docs-private#5) is approved. E1 shares no files with STRESS-7 (#63) or STRICT-Q (#64), so it can run alongside them within the two-open-PR limit. STRICT-Q's next step comes first. Any repair lands before N3 raises kernel capacity.

Completion criteria

  • E1–E4 results posted here, with each hypothesis confirmed or rejected
  • Either host cost per thread at 16 vCPUs within 2× of the 4-vCPU cost on the same QEMU build, with no regression in ring3-mm (1/4/8/16), the runtime groups, STRESS-7 or the shared-fault probes; or the excess recorded as an emulator limit with equal guest-side counts across QEMU builds

Risks and residual work

  • A repair to shootdown targeting must prove that a later CR3 load serializes (the RF178-33 race reasoning). It also needs a negative control that would keep a stale translation alive if the skip were wrong.
  • Per-address-space page-table locking must keep the ST-K2 lifecycle and shared-fault invariants.
  • At 16 vCPUs the guest clock advances at only 38–55% of host time, so attribution must use host time or TSC deltas.

Current status

2026-11-10: design under review (Zero-kernel/Nilix-docs-private#5). No code yet.

Activity

  1. added
    area: scheduler/concurrencyScheduling, CPU-local state, synchronization, RCU, and lock ordering
    performanceMeasured performance or resource-usage work
    trackingTracks an accepted multi-step project or release-gate item
    on Nov 10, 2026
  2. YUZHEthefool commented on Nov 11, 2026

    @YUZHEthefool
    MemberAuthor

    E2 results (workers fixed at 16; receipts in Zero-kernel/Nilix-docs-private#13)

    Diagnostic branch diag/c3-e2, not for merge: the E1 counters (#101), plus fixed thread workers and a /proc/schedstat copy every round. Each thread run was 300 s, alone on the dev host, under TCG. All nine runs are QUALIFIED; none is a pass.

    vCPUs Host ms per thread, 8.2 Host ms per thread, 6.2 IPIs per thread TLB IPIs per dispatch
    4 45.7 26.8 5.7 2.5
    8 87.8 54.7 12.1 5.6
    16 325.6 191.7 23.2 10.9

    The per-round schedstat copy inflates the absolute numbers, so compare them only with each other.

    • Workers are not the driver; vCPUs are. At fixed work, host cost per thread doubles from 4 to 8 vCPUs and grows ×3.5–3.7 from 8 to 16.
    • Guest events grow linearly.
      • TLB dispatches per thread are constant (2.1), and the broadcast share is a constant 56%.
      • IPIs per thread double with vCPUs. 93–98% of them are shootdowns, and none was coalesced.
      • From 8 to 16 vCPUs, host time per round tracks IPIs × vCPUs: within 4% on 8.2 and within 9% on 6.2.
    • H2 rejected. PT_LOCK is contended 5–8% of the time. Its waits fall from 4.5% of host time at 4 vCPUs to 0.6% at 16.
    • H3 rejected. Switches and migrations per thread fall as vCPUs rise (12.1 → 3.4).
    • H5: no growth. Guest/host wall ratio is 0.59–0.67 at every size.
    • H1 is a large constant factor.
      • Broadcasts carry about 77% of shootdown IPIs at 16 vCPUs.
      • E2b (2f3d94b, 4 vCPUs) splits them by cause: every CR3-0 broadcast, 86% of all broadcasts, is reclaim_kernel_stack's flush_all_address_spaces(). That is one full broadcast per thread lifetime.
      • Extrapolated to 16 vCPUs, that is about 65% of all IPIs.
    • H4 remains for the superlinear part.
      • At the end of the 16-vCPU runs, the 1-minute load average was only 3.5–6 on a 40-CPU host. That points to vCPU threads waiting rather than computing. It is a coarse signal.
      • E4 has to measure it.
    • QEMU builds. 6.2 is 1.6–1.7× faster than the dev host's 8.2 rebuild at every size, with the same shape. Treat this as a lead for E4, not a property of 8.2.
    • The kept idle_rearms lead.
      • The rate follows oversubscription: 2.8 per round at 4 vCPUs/16 workers, 0.3 at 16/16, and 0.3–0.4 with workers = vCPUs.
      • It is not a cause of the scaling.
      • Some wake path still leaves a runnable task in its own CPU's queue with no reschedule pending, and the [Tracking]: schedule a Ready pinned task left on an idle CPU (SCHED-STALL) #92 self-arm recovers it. Which path does this is still unknown.

    Next: E4 (host-side: per-thread QEMU CPU time and BQL/kick waits; official 8.2 vs 6.2). A repair design for kernel-stack reclaim (batching the flush across an RCU batch, or reusing stacks) is the first kernel-side candidate. It waits for the maintainer's direction.

  3. YUZHEthefool commented on Nov 11, 2026

    @YUZHEthefool
    MemberAuthor

    E2c and the repair design. E2c (diag/c3-e2 41b146d) shows that kernel-stack reclaims arrive one per RCU drain pass, spaced by the host time per thread: 99.5% of passes at 4 vCPUs, 100% at 16. At 16 vCPUs it reproduces the E2b split: 86.2% of broadcasts, about 65% of IPIs per thread. A batched kernel-stack TLB flush therefore has to span drain passes. The design for that repair is in Zero-kernel/Nilix-docs-private#15 (STOP-design), and the receipts are added to #13.

  4. YUZHEthefool commented on Nov 13, 2026

    @YUZHEthefool
    MemberAuthor

    Kernel-stack flush batch: results (#104; receipts in Zero-kernel/Nilix-docs-private#16)

    The repair designed after E2c is implemented in #104 and measured on diag/c3-kstack against diag/c3-kstack-base (each side plus E2's runner, not for merge). All runs are TCG on the dev host and QUALIFIED (3); none is a pass.

    The mechanism works as predicted. 16 workers, QEMU 8.2, per thread, parent → repaired:

    vCPUs IPIs sent Broadcasts PT_LOCK hold (ms) Stacks per batch
    4 5.65 → 2.91 1.176 → 0.225 2.52 → 1.55 16.0
    8 12.28 → 5.72 1.172 → 0.224 2.71 → 1.00 14.6
    16 23.46 → 11.13 1.175 → 0.334 5.28 → 1.48 5.6

    The design predicted about 11.1 IPIs per thread at 16 vCPUs. QEMU 6.2 shows the same.

    Host time per thread does not improve.

    • On the unmodified thread profile the repaired kernel is 1.6% slower at 4 vCPUs (42.2 → 42.8 ms) and 3.1% slower at 16 vCPUs (207.8 → 214.2 ms, three runs against two). The parent's own runs differ by about 2%.
    • In the E2 matrix it is 6–11% slower in the six comparable pairs. About 3 points of that are an extra schedstat line per round on the repaired side.
    • So E2's relation between IPIs × vCPUs and host time was a correlation only. H1 (broadcast shootdowns) is not a host-time driver, even as a constant factor.

    Lead for E3: the broadcast was also a wake-up. A diagnostic kernel that sends one reschedule IPI to every other CPU for each deferred stack (no flush, no acknowledgement wait) returns to the parent's time at 4 vCPUs: 50.2 → 47.2 ms per thread, parent 47.2. Each thread exit used to interrupt every CPU once. Something that becomes runnable or due around a thread's exit is picked up sooner when every CPU re-enters its scheduler at that moment. Which wake path depends on this is not known yet. It fits E2's kept idle_rearms lead.

    Leads for E4.

    • The timer tick. Seven of 43 dev-host boots calibrated the LAPIC timer 17–46% high (one 10 ms HPET sample at boot), always toward a slower tick. Those runs were 4–11% faster. At 16 vCPUs one of them reached the stress runner in 97 s instead of 236 s, with a third of the boot-phase interrupt trace. The tick is a first-order host cost under TCG, and init_count has to be checked in every host-time comparison on this host.
    • Host-side thread state. At 16 vCPUs each vCPU thread uses 0.68 of a host CPU: 0.44 in the host kernel and 0.24 in user mode, with about 7400 voluntary context switches per second. At 4 vCPUs the split is 0.11 against 0.62 with about 2700. Run-queue wait is under 0.3%. That is sleeping and waking on host locks, not emulation, and it is the same on both kernels.

    #104 is held before merge for a maintainer decision: it is correct and removes the broadcast from the PT_LOCK section, but it is not a host-time repair on its own.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: scheduler/concurrencyScheduling, CPU-local state, synchronization, RCU, and lock orderingneeds: triageNeeds maintainer classification, verification, and priorityperformanceMeasured performance or resource-usage worktrackingTracks an accepted multi-step project or release-gate item

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions