Local genetic variance engine (Module 02)
genboostgpu lgv computes, for every region (VMR, CpH region, tile), the
features of the frozen Module 02 joint model of the
dna-methylation-heritability analysis, applies that model, and derives the
within-cell relative local SNP contribution score. It replaces the R array
job of Module 02 Stage 01 and reproduces Stages 02–05, writing tables with the
R pipeline’s file names and columns so a run can still be sealed by the R
Stage 06.
Important
The score is a relative ranking within one cohort × region cell. Absolute
locus-level PVE is not identifiable at these sample sizes
(PASS_RELATIVE_GENETIC_CONTROL / FAIL_ABSOLUTE_LOCUS_PVE); every output
row carries absolute_pve_interpretation_allowed = FALSE.
What is computed per region
Feature |
Definition (identical to the R pipeline) |
|---|---|
|
Nested out-of-fold elastic net: 5 outer folds × 2
repeats, inner 5-fold |
|
Haseman–Elston regression of pairwise residual products on GRM relatedness. |
|
Effective rank |
|
Median adjacent-SNP r² over ≤200 evenly spaced pairs. |
|
GEMMA 0.98.5 BSLMM (10k burn-in, 100k sampling, rpace 10) on mean-imputed dosages and covariate-residualized phenotype. |
Fidelity to R
glmnet — the coordinate-descent path solver of glmnet 4.1-10 (covariance and naive modes, strong rules, KKT checks,
fix.lam) is ported line for line, including the summation order of Eigen’s vectorized dot products (SSE2, as in the conda-forge/CRAN x86-64 builds). On the CPU, paths, intercepts and deviance ratios are bitwise identical to R with identical pass counts.cv.glmnetreproduces fold fits on their own lambda paths andlambda.interp;cvm/cvsdagree to ~1e-15 (R’s predictions go through its BLAS) andlambda.min/lambda.1seare R’s.GPU — the warp-per-problem kernel runs the same algorithm but reduces across lanes and lets CUDA fuse multiply-adds, so it agrees with the CPU path to rounding (~1e-15 per step). Like any rounding difference, that can occasionally resolve a near-tied convergence or
lambda.1sedecision differently. Use--device cpuwhen a bit-exact replay is the goal.Random numbers — R’s Mersenne-Twister seeding and
sample()rejection sampling are ported bit for bit, so fold assignments (and Module 03 permutations) equal R’s without exporting anything.Inputs — the analysis-repo adapter reads per-VMR BEDs with bigsnpr’s allele coding and orders donors exactly as R’s
merge(); genotype matrices are bitwise R’s.SNP screen — the top-1500 marginal-correlation screen sums in a fixed order, so SNPs with identical training genotypes (common: dozens of copies in strong-LD windows) get identical scores and keep their column order, on any CPU, GPU or thread count. R computes the same scores with its BLAS (OpenBLAS
dgemv), whose last-bit noise decides the order of such ties.What R itself does not reproduce — R’s covariate residualization (
lm.fit) and screen go through OpenBLAS, whose kernels depend on the CPU. Re-running R on identical inputs withOPENBLAS_CORETYPE=Haswellinstead ofSkylakeXchangesrho2_oofof some VMRs by up to ~5e-4, because a 1e-15 change in the adjusted phenotype flips a near-tiedlambda.1se. Replays therefore match sealed runs exactly for HE,p_eff, LD and statuses, and match the nested-EN features within that CPU-to-CPU envelope (see below).BSLMM — GEMMA’s
hchain is reproduced exactly; its reportedpveadditionally depends on the OpenBLAS kernel GEMMA ran with. The GEMMA binary links OpenBLAS 0.3.9 built withDYNAMIC_ARCH, which picks a kernel from the CPU:SkylakeXon quest10 nodes, where the sealed R runs ran, but the genericPrescottfallback on quest13 (Xeon 8592+, a CPU 0.3.9 does not know). The two differ by up to 1.3e-3 inbslmm_pve. GENBoostGPU pins the kernel withOPENBLAS_CORETYPE(--gemma-blas-coretype, defaultSkylakeX; see GEMMA’s OpenBLAS kernel (--gemma-blas-coretype)) and refuses to start GEMMA on a CPU without AVX-512. With the pin,bslmm_pvematches the sealedlgv-all_individuals.EA-caudate-20260917values to 1e-16 on both node types.
Replay of an accepted run
500 random tasks of lgv-all_individuals.EA-caudate-20260917 (n = 129;
489 completed, 11 qc_failed), replayed on CPU nodes and compared row by row
with the sealed tables:
Statuses, HE, |
identical (≤ 1e-13) |
|
identical (≤ 1e-16) |
|
62 % bitwise; 3.5 % differ > 1e-4; max 4.6e-3; Spearman 0.999998 |
|
57 % bitwise; 2.7 % differ > 1e-4; max 1.6e-3; Spearman 0.9999999 |
|
replayed rows inserted in the full cell (11,249 VMRs): max difference 0.0033, Spearman 0.99999999, no quartile changes |
For the 106 VMRs where GENBoostGPU and the sealed run disagree, plus 20 that agree, R itself was re-run on identical inputs with a different OpenBLAS kernel: R vs sealed R differs > 1e-4 in 17 VMRs (max 2.0e-3), GENBoostGPU vs sealed R in 17 VMRs (max 4.6e-3). The differences are the size of R’s own CPU-to-CPU variation; the slightly longer tail comes from the order of tied SNPs in the screen.
Running on the analysis repository
Export the frozen model once (R, no JSON package needed):
Rscript scripts/export_frozen_joint_model.R \
--model <repo>/02_local_genetic_variance/_m/runs/lgv-joint-pve-train-20260820/combined/joint-pve-calibrator.rds \
--sha256 9f26c3273746fda85d9bbf21e224857db9a1ad79a521582a12f241854c03223a \
--out joint-pve-model.json
Replay an accepted Module 02 run (same tasks, donors, seeds and support):
genboostgpu lgv init --run-dir runs/replay-AA-caudate --run-id gbg-replay-AA-caudate \
--replay <repo>/02_local_genetic_variance/_m/runs/lgv-AA-caudate-rescore-20260913 \
--joint-model joint-pve-model.json
GBG_RUN_DIR=runs/replay-AA-caudate GBG_N_SHARDS=8 GBG_ACCOUNT=p32505 \
scripts/slurm/lgv_submit.sh gpu
--task-ids or --limit make a smoke run (Stage 05 then returns
PASS_SMOKE_ONLY_NOT_ACCEPTABLE). A new run on an accepted Module 01 run or
01b estimation cell uses --dnam-upstream <run dir> --tasks <task table>
--cohort --region plus --support and --joint-model.
Generic regions (CpG/CpH regions, tiles)
genboostgpu lgv init --run-dir runs/cph-caudate --run-id cph-caudate-v1 \
--regions build/regions.tsv --phenotypes build/phenotypes.parquet \
--genotypes '/path/LIBD.chr{chrom}.AA' --covariates covs.tsv \
--numeric-covariates age,snpPC1,snpPC2,snpPC3 --factor-covariates sex,diagnosis \
--cohort AA --region caudate --bslmm inline \
--joint-model joint-pve-model.json --support joint-pve-characterized-support.tsv
--genotypes is either a per-chromosome pattern with {chrom} or one
genome-wide prefix (e.g. inputs/genotypes/TOPMed_LIBD.AA); .bed and
.pgen (pip install genboostgpu[plink2]) are detected. From a
genome-wide file only the current chromosome’s variants are read and held as
int8 (chr21 of the TOPMed AA .pgen: 212k variants × 526 donors, 20 s).
--genotype-id-column picks the .fam/.psam column matched to
sample_id (FID when it holds BrNums). Donors are matched by ID across phenotype, covariate
and genotype tables; variant QC (MAF ≥ 0.05, missingness ≤ 0.05) is computed
on the analysis donors.
Warning
The frozen model is characterized only for the cells and sample sizes in
the support table (allowed_n). Regions of a new cohort or tissue are
scored only where the domain gate passes; anything else is reported as
outside_domain rather than ranked.
Run directory
run.json (all settings, seeds policy, software versions), tasks.tsv,
task_rows/part-*.parquet (one terminal row per task, append-only, so a
killed shard resumes), and after combine: results/combined/
observed-joint-features.tsv, task-reconciliation.tsv,
observed-joint-estimates.tsv, local-genetic-control-{cell}-{region}-vmrs.tsv,
observed-score-qc.tsv and results/run-manifest.json (decision, input
and output SHA-256s). --write-r-task-rows also writes
results/task_rows/vmr-%07d.tsv for R Stage 02.
BSLMM placement
--bslmm inline runs GEMMA chains in a CPU process pool on the GPU node
while the GPU solves the elastic net; --bslmm separate defers them to a
CPU-only array (genboostgpu lgv bslmm) so GPU allocations never wait on
MCMC; --bslmm off produces features only (terminal_status =
features_only) and no score.
GEMMA’s OpenBLAS kernel (--gemma-blas-coretype)
How it works. The GEMMA binary the pipeline uses
(/projects/p32505/opt/bin/gemma, GEMMA 0.98.5) is statically linked
against OpenBLAS 0.3.9 built with DYNAMIC_ARCH: it carries one set of
linear-algebra kernels per CPU family and chooses one when it starts, from the
CPU it finds. The environment variable OPENBLAS_CORETYPE overrides that
choice. genboostgpu lgv init and genboostgpu sites init take
--gemma-blas-coretype KERNEL, store it in run.json as
bslmm.blas_coretype, and every GEMMA chain of the run is started with
OPENBLAS_CORETYPE=KERNEL. auto sets nothing and leaves the choice to
OpenBLAS. The default is SkylakeX.
Why it matters. The kernels compute the same quantities but round
differently, and BSLMM’s MCMC carries those last-bit differences forward: the
sampled h chain is unchanged, but bslmm_pve moves by up to ~1e-3
between kernels (median ~1e-6). That is far below its posterior uncertainty,
but it means two runs on different hardware are not identical. The sealed
dna-methylation-heritability Module 02 runs ran on Quest quest10 nodes,
where OpenBLAS picks SkylakeX. On quest13 nodes (Xeon 8592+, a CPU
OpenBLAS 0.3.9 does not recognise) it falls back to the generic Prescott
kernel, and a replay of lgv-all_individuals.EA-caudate-20260917 differed on
every locus run there. Pinning SkylakeX made both node types reproduce the
sealed values to 1e-16.
Which CPUs can run it. SkylakeX needs AVX-512 (avx512f, cd,
bw, dq, vl): Intel Xeon Skylake-SP and later (Cascade Lake, Ice
Lake, Sapphire/Emerald Rapids) and AMD Zen 4 and later (EPYC Genoa/Turin).
AMD Zen 2/3 (EPYC Rome/Milan), most desktop Intel CPUs and non-x86 machines do
not have it. A shard checks the CPU before any task runs and stops with an
error naming the missing flags, rather than letting GEMMA crash on an illegal
instruction or quietly use another kernel. To check a node:
grep -o -w -E 'avx512(f|cd|bw|dq|vl)' /proc/cpuinfo | sort -u
When to change it.
Situation |
Setting |
|---|---|
Replaying or extending a sealed run; any production |
|
run that will be compared with existing GENBoostGPU or |
|
R results |
|
Compute nodes without AVX-512 (e.g. AMD Rome/Milan) |
|
A GEMMA build linked to MKL, a system BLAS or another |
|
OpenBLAS version |
ignored or means something else there) |
Rules that follow from this:
Choose the kernel once per study and keep it for every run whose
bslmm_pve(and therefore score) will be compared.Haswellis reproducible on any AVX2 CPU, so it is the portable choice for a new study with no sealed runs to match;autois reproducible only on one CPU model.Do not change the kernel of a run that has already written task rows: its rows would mix kernels. Initialize a new run instead (editing
bslmm.blas_coretypeinrun.jsonis safe only before any task runs).Names are checked against OpenBLAS’s list (
Haswell,SkylakeX,Zen, …; case-insensitive). OpenBLAS itself ignores an unknown name and silently falls back to auto-detection, sogenboostgpurejects it atinit.To see the kernel a GEMMA binary actually uses, run it once with
OPENBLAS_VERBOSE=2; OpenBLAS printsCore: <kernel>.