Parallel founder scaling diagnosis
Source:vignettes/articles/docs-qualification-parallel-founder-scaling-diagnosis.Rmd
docs-qualification-parallel-founder-scaling-diagnosis.RmdHistorical evidence: these measurements retain their recorded revisions, fixtures and limitations; website rendering does not rerun scientific qualification.
Decision
The committed 0.12.0 founder implementation is healthy (decision class A). On this controlled workload, packed materialization sped up 1.60x, 2.50x, and 3.43x with 2, 4, and 8 threads. Workers were active and well balanced, setup and join costs were negligible, output writes were timed separately, and static inspection found neither synchronization in the hot loop nor overlapping output writes. No production code or package version changed.
The flattening beyond four threads is most consistent with the cost of shared, irregular donor-panel reads and marker-major output read/modify/write traffic meeting cache/memory and heterogeneous-core frequency limits. Reliable hardware cache-miss, bandwidth, and core-frequency counters were not available on this Windows installation, so the measurements do not distinguish those effects individually. This is deliberately a cautious locality/platform diagnosis, not a measured DRAM-bandwidth claim.
Entry and environment
The measurement was made on 2026-09-05 from branch main,
commit 24c8921c40ba5db250c3db6dffec8a78ea2f1ad0, package
version 0.12.0. The worktree and index were clean at entry. The four
public workflow functions gsim_import_vcf(),
gsim_reference(), gsim_simulate_founders(),
and gsim_simulate_pedigree() were present. A small
committed public founder smoke workflow passed; its 4-reference,
8-marker, 8-founder HAP had MD5
26b1877ac5def6ed8e4f3dadcea9f030.
The test machine was Windows 11 x64 build 26200 with a 13th Gen Intel Core i7-1365U (10 physical, 12 logical, hybrid cores), 16,794,288,128 bytes physical memory, R 4.4.1, and GCC/G++ 13.2.0 using C++17. The processor’s registry nominal frequency was 2,688 MHz. BLAS, OpenMP, and MKL thread environment settings were fixed at one; only gsim’s explicit founder workers varied.
The previously prepared 1000 Genomes HAP panel was not present at the
available external paths. Following the qualification fallback, the run
used a deterministic external HAP fixture with 500 reference individuals
and 49,000 markers on chromosome 22. Its phases use varying
allele-frequency thresholds and deliberately asymmetric deterministic
patterns. Genetic position runs linearly from 0 to 5 cM. The reference
HAP was 6,272,112 bytes, with MD5
c8b31f900813da89f70b9e77a9d31fd4; its two packed biological
planes occupy 6,272,000 bytes. This fixture is realistic in dimensions
and allele variation, but it is not the 1000 Genomes LD structure. All
fixture and output files lived outside the repository and were removed
after qualification.
Frozen workload and method
The primary workload generated 10,000 founders across 49,000 markers,
with one ancestry of weight 1, N=500,
Ne=10000, rho=0.02, mutation age
1e9, seed 20260905, batch size 4,096, and
phased HAP output. One warm-up of 512 founders at eight threads preceded
measurement. VCF import was excluded.
Each 1/2/4/8-thread invocation opened and loaded the same prepared
HAP, planned the same events, materialized the complete workload three
times, and wrote one output. The table reports the median
materialization time and the independently accounted single-pass
workflow time. The materialization ranges expose run noise. R process
user and system CPU times were recorded with proc.time().
An external PowerShell sampler measured peak process working set every
100 ms. Filesystem cache effects were not controlled; HAP load and
output timings must therefore be read as bounded workflow observations,
not storage benchmarks.
Every primary output was byte-identical: HAP MD5
6ed6cc03d1524334d7556d0b366aa835, BIM MD5
4e4206cb4b90985b5b5591ce96e3cf3c, FAM MD5
97d8672dfb0a4223e93fbcdef22461e2, and HAP size 123,088,112
bytes.
Thread scaling
| Threads | Materialization median, s (range) | Material CPU, % | Speed-up | Efficiency | Material throughput, founder-markers/s | Single-pass total, s | Total speed-up | Peak working set, bytes |
|---|---|---|---|---|---|---|---|---|
| 1 | 40.98 (37.76–42.16) | 97.5 | 1.00 | 100.0% | 11,957,052 | 48.01 | 1.00 | 173,428,736 |
| 2 | 25.69 (25.62–25.75) | 193.3 | 1.60 | 79.8% | 19,073,569 | 32.43 | 1.48 | 176,377,856 |
| 4 | 16.42 (16.09–16.63) | 375.6 | 2.50 | 62.4% | 29,841,657 | 23.40 | 2.05 | 186,638,336 |
| 8 | 11.96 (11.55–12.24) | 578.3 | 3.43 | 42.8% | 40,969,900 | 18.99 | 2.53 | 194,322,432 |
The 8-thread process consumed about 5.8 effective cores during materialization, despite seven spawned workers plus the calling thread. This is consistent with the hybrid processor and a shared-data locality ceiling. It is not consistent with a single large serialized critical section: utilization grew through all thread counts and materialization wall time continued to fall.
Single-pass stage times were:
| Stage, seconds | 1 thread | 2 threads | 4 threads | 8 threads |
|---|---|---|---|---|
| HAP open | 0.56 | 0.53 | 0.51 | 0.53 |
| HAP chromosome load | 3.80 | 2.94 | 3.09 | 3.28 |
| Founder event planning | 1.52 | 1.30 | 1.44 | 1.48 |
| Packed materialization | 40.98 | 25.69 | 16.42 | 11.96 |
| HAP initialization | 0.29 | 0.42 | 0.22 | 0.25 |
| Positional batch writes | 3.51 | 4.59 | 4.95 | 4.54 |
| HAP finalization | 0.46 | 0.67 | 0.65 | 0.65 |
These independently timed boundaries show that output I/O did not contaminate the materialization number. Planning, initialization, writing, and finalization are serial in this experiment and explain why total speed-up is lower than materialization speed-up.
Batch sensitivity and memory
One complete workflow was run at batch sizes 1,024 and 8,192 in addition to the primary 4,096 case. All batch/thread outputs had the same HAP/BIM/FAM hashes as the primary run.
| Batch | 1-thread materialization, s | 8-thread materialization, s | Speed-up | 8-thread total, s | Reference + batch payload, bytes | 8-thread peak working set, bytes |
|---|---|---|---|---|---|---|
| 1,024 | 26.49 | 7.08 | 3.74 | 20.60 | 18,816,000 | 158,298,112 |
| 4,096 | 40.98 | 11.96 | 3.43 | 18.99 | 56,448,000 | 194,322,432 |
| 8,192 | 44.32 | 12.69 | 3.49 | 18.51 | 106,624,000 | 238,219,264 |
The smaller batch’s much lower materialization time is strong locality evidence, although its additional positional HAP writes make its total eight-thread time slower. Batch 8,192 improved total time only 0.48 seconds (2.5%) relative to 4,096 while adding about 50 MB of packed biological payload. Batch 4,096 remains a sound memory/latency default.
The primary maximum event-plan object was 3,682,632 bytes. No
complete founder population, dense haplotypes, genotype matrix,
per-thread reference copy, or per-thread output batch was allocated.
With fixed batch size, working biological payload retains the contract
O((R+B) M_c / 8 + S_B) for two phases; total founder count
affects the on-disk output and number of sequential batches, not the
largest packed batch.
Native diagnostics
Static inspection of materialize_founders() established
this execution model:
- one native materialization call per founder batch;
-
worker_count - 1newstd::threadobjects per call, with the caller doing worker zero, followed by one join for each spawned worker; - deterministic static partitioning into contiguous, disjoint 64-sample packed output words;
- distinct H1 and H2 destination planes and no overlapping worker writes;
- a single immutable shared reference panel;
- no mutex, atomic counter, R API call, shared RNG, or heap allocation in the interval-copy hot loop.
The three primary batches consequently created 0, 3, 9, and 21 native threads at 1, 2, 4, and 8 requested threads. Aggregated minimal-work probes estimated setup/join cost per batch at approximately 0.97, 1.61, and 1.94 ms for 2, 4, and 8 threads. Thus all eight-thread setup/join activity was under 6 ms against 11.96 seconds of materialization.
Analytical worker-balance coefficients of variation across the whole primary workload were:
| Threads | Output words CV | Segment operations CV | Marker visits CV |
|---|---|---|---|
| 2 | 0.0090 | 0.0017 | 0.0023 |
| 4 | 0.0127 | 0.0084 | 0.0032 |
| 8 | 0.0264 | 0.0299 | 0.0260 |
The small residual variation is dominated by the final partial batch. A separate 10 ms operating-system thread sample on one 4,096-founder, eight-thread batch observed three materialization repetitions. In each repetition, the seven spawned workers lived for approximately 5.05–5.29 seconds; the last worker finished only 0.10–0.15 seconds after the first (under 3% of call duration). This bounds join-tail idle time and rules out severe worker imbalance.
False sharing at partition boundaries cannot be proven absent because allocator base alignment was not instrumented. It is unlikely to be dominant: partitions never share a 64-bit output word, and the 1,024 batch gives each of eight workers only two adjacent words per marker–the most boundary-sensitive case–yet it had the fastest absolute materialization and the best eight-thread speed-up.
The dominant operations remain phase-specific reads from segment-selected donor rows and marker-major output updates. Donor changes make the shared reference access irregular, while each output word is repeatedly updated across different individual segments. The strong batch-size response and diminishing effective CPU utilization therefore support a cache/memory-locality limit. All-core turbo reduction and scheduling onto the i7-1365U’s heterogeneous cores may contribute. Windows Performance Recorder/xperf or equivalent reliable cache/bandwidth counters were not available, and no profiler or administrator access was added.
Correctness and recommendation
The controlled benchmark proved exact HAP, BIM, and FAM bytes across every thread and batch setting. The package’s focused separated-simulation parity test additionally checks phase-asymmetric donors, segment audits, batch divisions, 1/2/4/8-thread equality, chromosome identity, R RNG isolation, pedigree output, and Mendelian consistency. Those results are recorded with the final validation for this milestone.
No targeted production correction was justified. Thread creation, joining, synchronization, load balance, false sharing, partitioning, serial work in the materialization timer, and output contamination were each either measured small or contradicted by the observations. Version 0.12.0 is retained.
For a typical chromosome job, four threads provide the better efficiency and eight provide the shortest measured latency. Use four when running concurrent chromosomes or other jobs; use eight for a single latency-sensitive chromosome when memory bandwidth and CPU capacity are otherwise idle. Prefer multiple independent approximately four-thread chromosome jobs over adding more than four threads to one chromosome, subject to aggregate memory and storage-I/O limits. This is an operational recommendation, not a request for process-level sharding inside gsim.
The next smallest experiment, only if more speed is required, is a hardware- counter run on a platform with available cache-miss, memory-bandwidth, and per-core frequency telemetry. Do not change packed copying or add SIMD before that evidence exists.
Final validation
Because production code did not change, validation used the
measurement-only path authorized for this milestone rather than a
separate duplicate full-suite run.
test-separated-simulation.R passed all 26 focused
expectations. It covers the public
VCF-to-reference-to-founders-to-pedigree workflow, exact HAP bytes and
phases across batch sizes and 1/2/4/8 threads, ordered segment audits, R
RNG isolation, different-seed divergence, exact BED dosage, and zero
Mendelian inconsistencies.
A fresh isolated R CMD INSTALL --preclean installed and
loaded gsim 0.12.0. A built-source-tarball
R CMD check --no-manual then completed with status
OK; that check also ran the package test suite once.
git diff --check passed. Compiled objects, DLLs,
check/install trees, benchmark outputs, and the external synthetic
reference were removed after validation.
Reproduction
tools/benchmark/benchmark_parallel_founders.R takes an
external HAP prefix, an external output directory, and a thread count.
Its environment variables are GSIM_PARALLEL_FOUNDERS,
GSIM_PARALLEL_BATCH_SIZE,
GSIM_PARALLEL_MATERIALIZATION_REPS,
GSIM_PARALLEL_PROBE_REPS, GSIM_PARALLEL_SEED,
and GSIM_PARALLEL_KEEP_OUTPUT. It never downloads data and
rejects an output directory inside the repository. With output retention
disabled, it hashes and then removes each generated HAP/BIM/FAM
triplet.