boot-sp PR-4: K>1 batch executor (groups of K ranks; wheel per group) - #18
Draft
DLWoodruff wants to merge 23 commits into
Draft
boot-sp PR-4: K>1 batch executor (groups of K ranks; wheel per group)#18DLWoodruff wants to merge 23 commits into
DLWoodruff wants to merge 23 commits into
Conversation
DLWoodruff
force-pushed
the
bootsp-pr-d
branch
2 times, most recently
from
July 26, 2026 15:25
34c6293 to
15bedb8
Compare
DLWoodruff
force-pushed
the
bootsp-pr-d
branch
2 times, most recently
from
July 26, 2026 21:24
c394621 to
649acf2
Compare
Pure refactor: pull the full generic_cylinders option-registration sequence out of parse_args into a reusable register_generic_args(cfg, module) so a second consumer -- the bootstrap batch config file, which is literally a batch generic_cylinders configuration -- can build an identical Config and then read its options from a file rather than the command line. parse_args now just calls it and parses argv. No behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New mpisppy/confidence_intervals/bootsp/batch_executor.py. A batch is solved by a group of K ranks (--boot-ranks-per-batch); the bootstrap partitions the R ranks into G = R // K groups that run concurrently, each solving its share of the batches in sequence, with the results gathered to rank 0 (design 9.4). The executor owns only the rank arithmetic and the collectives: it exposes each group's communicator (for the wheel / xhat evaluation), a leaders-only communicator (for the cross-group Gatherv), the per-group batch share and rng seed, and a gather() that assembles the per-group results on global rank 0. The two endpoints are the same mechanism -- K=1 is G=R (each rank its own group, a direct EF per batch) and K=R is G=1 (one group, a wheel per batch in sequence). K=1 reproduces the original per-rank behavior bit-for-bit, so the standalone drivers are unaffected. K must divide the rank count (leftover-rank handling is a scheduled refinement). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The empirical estimators (classical/subsampling/extended/bagging and their resample helpers) now take a BatchExecutor (default K=1) instead of the module COMM_WORLD/rankcomm globals: group seed and batch share from the executor, the group communicator for the solves and xhat evaluations, and executor.gather in place of the five repeated Gatherv blocks. A single _batch_optimal_value is the one entry for the batch optimal L_b -- a direct-EF best bound at K=1, the wheel outer bound (via executor.batch_optimal_solver) at K>1 -- so the estimator math is identical across K. extended's center block gates on group 0 (its solves are collective on the group) rather than on rank 0. evaluate_routine/evaluate_scenarios gain an optional mpicomm (default rankcomm) so the smoothed methods and the simulation prep, which are K=1 only, keep their old behavior. solve_routine, slice_lens and the module globals are retained for them. The standalone user_boot/simulate_boot paths are unchanged (their tests pass with the default K=1 executor). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New mpisppy/generic/boot_batch.py. For K>1 a resampled batch is solved by a full hub/spoke wheel rather than a direct EF. How that wheel is configured is a different problem from the xhat solve (a batch has N scenarios, usually far more than the M candidate records), so it is supplied separately as a file of generic_cylinders flags: - parse_batch_config_file reads the file as a generic_cylinders command line (via register_generic_args + create_parser; # starts a comment), so it is literally a batch generic_cylinders configuration. - make_batch_optimal_solver returns the callable the executor invokes per batch: it builds a wheel over the batch's sample scenarios (reusing the generic hub/spoke/decomp builders with injected SampleScenario names and the positional sample->record mapping), runs it on the group communicator, and returns the wheel's outer (decomposition) bound as L_b. The bound is shared across the group with allgather (WheelSpinner reports it only on hub ranks); a config that yields no outer bound (a bare PH hub) is rejected with a clear message. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
config.boot_args gains --boot-batch-config-file (required for a bootstrap run) and clarifies --boot-ranks-per-batch (K); it retires the interim --boot-solver-name / --boot-solver-options, whose role the batch config file now fills. do_boot no longer rejects K>1: it parses the batch config file, builds the executor first (so K is validated against the rank count before the K>1 wheel solver, which validates the batch rho), attaches the wheel solver only when K>1, and passes the executor to compute_ci. The estimator's solver name and options now come from the batch config file (they govern the K=1 EF and the xhat-evaluation solves, so both agree with the K>1 wheel). boot_requested now errors when no batch config file is given. test_boot_generic: batch config fixtures (per-rank, with teardown), a config-file solver-role test replacing the retired boot-solver test, a batch-config-required test, a bad-ranks-per-batch test, and test_do_boot_g1_wheel_matches_ef -- the G=1 checkpoint (K=n_proc). It is self-validating: the wheel path reproduces the K=1 EF value at xhat exactly (K-invariant), its outer bound sits at or below the EF optimum, and the reported gap is conservative. Gated on n_proc>=2, so it runs under the existing mpiexec -np 2 test_boot_generic invocation with no harness change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
boot_sp.rst: document --boot-batch-config-file and its role split from the xhat solve, add the "MPI ranks: groups of K" section, drop the retired --boot-solver-* rows, and update the example to pass a batch config file. schultz_data_boot.bash writes a one-line (K=1) batch config to a temp file. The design doc status and section 9.4 are marked implemented (the G=1 checkpoint and G>1 validated). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
examples/bootsp/schultz_data/schultz_data_boot_cylinders.bash runs the wheel-per-batch path end to end: 6 ranks find xhat together (PH hub + xhatshuffle + lagrangian), then re-form into G = 2 groups of 3 that solve the bootstrap batches concurrently, each batch by a subgradient wheel configured in the committed schultz_wheel_batch.txt. The solver flows through so it runs with any MIP/MIQP solver. boot_sp.rst points at the demo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…oot_sp.rst The "In generic_cylinders" section's first mention of the driver now links to its own page via :ref:`generic_cylinders`, matching how the other docs point at it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A batch was described as having "N scenarios, usually far more than the M candidate records." That is wrong: M (candidate_sample_size) and N (sample_size) are independent user choices with no inherent ordering (only M + N <= dataset size is enforced), and the batch scenario count is N only for the classical and extended methods -- it is the subsample size for subsampling and bagging. The real reason the batch is configured separately is simply that it is a different instance: a resample of the data, sized independently of the candidate records. Reworded across boot_sp.rst, the design doc, boot_batch.py, and the --boot-batch-config-file help. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The K=1 and K>1 batch config examples shared a single literal block, so they read as the contents of one file (which would be nonsense -- two --solver-name lines, and the two cases never coexist). Split them into two labelled blocks with a sentence making clear they are alternatives (one file per run), and trim the now-redundant "K=1 needs only a solver / K>1 needs a wheel" prose to just the outer-bound requirement. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… cap Address three gaps in the "MPI ranks: groups of K" subsection: - K must not only divide the rank count but, for K>1, be a multiple of the number of cylinders in the batch config (WheelSpinner splits the group's K ranks evenly among the hub and spokes). Documented in the flag table and the subsection; a two-cylinder batch needs an even K. - The K>1 example was a bare subgradient hub with no spokes; make it a real hub-and-spoke wheel (PH hub + Lagrangian bounding spoke). - Because the G groups run concurrently, add --max-solver-threads to the K>1 example and explain that it keeps the concurrent solves from oversubscribing the cores. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
"wheel" was overused and is not the house term: outside boot_sp.rst the docs describe this system as hub-and-spoke / cylinders (WheelSpinner is only ever named as the API object). Replace the seven prose uses with "cylinders" / "a cylinder solve" / "cylinder configuration". Not "multi-cylinder", since a K>1 batch can be a single cylinder spanning K ranks (the subgradient-hub demo). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The page was ~12 flat top-level sections. Group them into five: an intro that now includes a roadmap paragraph, then Background (Methods, The model module, The optimality gap), Standalone drivers (Modes, Arguments, Batch parallelism, boot_general_prep, Examples), In generic_cylinders (Options, The batch config file, MPI ranks: groups of K, Example), Smoothed methods, and References. - The outer-bound paragraph moves out of "Batch parallelism" into a Background subsection "The optimality gap" (it is interface-agnostic). - The two example sections merge under Standalone drivers > Examples (On-the-fly data / From a dataset file). - The generic_cylinders material gains subsections. - "Batch parallelism" now hyperlinks to the "MPI ranks: groups of K" section. Content (tables, code blocks, citations, examples) is unchanged; only structure and the new roadmap paragraph. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reorder the Examples subsections so the data-based example (schultz_data) comes
first, matching the page's framing; the on-the-fly schultz example follows,
reworded to say plainly what it is (data computed from the scenario number, no
dataset file) and flagged as an unusual, testing-oriented example. Drops the
non-sequitur "reproducible across solvers" claim. Also fixes an intro typo
("boostrap") and a missing comma.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In the generic_cylinders hold-out split the candidate block (M) and the resampling pool (N) are disjoint, so M + N cannot exceed the dataset size; normally they add up to it so no record goes unused. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rd pool) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The generic_cylinders runs (the xhat solve in the docs' end-to-end example and in both schultz_data bash demos) and the K>1 batch config run cylinders, so several solvers are in flight at once; cap threads per solver to avoid oversubscribing the cores. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The maximization guard, the conservatism wording and the outer-bound fallback all landed in PR-2/PR-3, where the code they correct lives. Three pieces only make sense once cylinders solve the batches, so they land here: - do_boot probes one scenario for its objective sense. solve_routine makes the same check, but only K=1 builds an extensive form for it to look at; a K>1 batch is solved by a wheel, so nothing else would catch a maximization model on that path. Section 9.4.1 updated to say so. - _best_bound's docstring can now name its K>1 sibling, boot_batch._outer_bound_over_group, and why that one raises where this one falls back: an extensive form has an incumbent to offer in place of an unproved bound, and a wheel run for its outer bound does not. - the rst's optimality-gap section, which the nested-structure reorganization relocated, gets the corrected wording back, and can point at --rel-gap in the batch config file (a PR-4 flag) as the lever for tightening the batch solves. Also design 4.11, which named boot_sp._batch_rng: this PR removed it in favor of BatchExecutor.group_seed, so the reference is updated to the two streams that now exist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked PR — fork-only for now. Base is
bootsp-pr-c(PR-3), so this diff isPR-4 alone. The branch itself carries the whole boot-sp stack (PR-1→4), so
git checkout bootsp-pr-dis self-contained and runnable. Opens upstream onlyafter PR-2 (now upstream Pyomo/mpi-sppy#818)
and PR-3 land.
This branch was force-pushed again (2026-07-26). A review against the
Chen & Woodruff papers found seven issues; the fixes were placed on the lowest
branch whose code they correct, so
bootsp-pr-b(PR-2) andbootsp-pr-c(PR-3)were both rewritten and this branch was rebased onto the new
pr-c.Re-sync, don't merge. Every commit SHA is new (previous published tip
15bedb80→c3946212; the tip before that was5b4e48b6). Reset yourcheckout onto the new tip:
A plain
git pullwill try to merge the old and new histories.Smoothed bootstrap results change substantially. Two deviations from
CW2024 Algorithm 3 were corrected in PR-2, which this branch inherits: the
batches were overlapping sliding windows rather than independent resamples, and
the center was the empirical gap rather than the smoothed one. On the
cvarexample at
N=20, nB=10the interval widens about fivefold — the previoussmoothed intervals were far too narrow.
Smoothed_baggingis unaffected exceptfor a
ddof=1correction to its variance. The empirical methods are unchanged.Bootstrap CI bounds change; point estimates do not. No CLI/flag changes —
the same commands run. But the interval endpoints move, from two fixes the
branch now inherits from
main(they postdated this branch's old base):to compute
xhat. The interval is estimated out-of-sample (on data that didnot choose
xhat) rather than optimistically in-sample, so it is honest andgenerally a touch wider.
so the bounds shift. (The
K > 1batch executor's per-group seeding changedtoo, so those runs move as well.)
The gap point estimates (the centers) are unchanged — they are deterministic.
Only recorded/expected CI bounds need updating.
New requirement. Sample splitting needs
sample_size + candidate_sample_size ≤ max_count. A config that previouslyover-subscribed the dataset now fails fast with a clear error instead of
silently overlapping the candidate and CI records.
The generic_cylinders bootstrap now honors sample splitting end to end —
do_bootholds theMcandidate records out of theN-record poolpositionally, so the integrated
--boot-*path is consistent with thestandalone estimators.
You also inherit current upstream
main(CVaR η-bound, CI ruff pin, etc.),since the branch was rebased onto it.
What this adds
The
K > 1batch executor for the data-based bootstrap ingeneric_cylinders(design
doc/designs/bootsp_merge_design.md§9.4). A resampled batch is nolonger only a per-rank extensive form: it can be solved by a group of
Kranks running a wheel. The
Rranks partition intoG = R // Kgroups thatrun concurrently, each solving its share of the batches in sequence, with the
results gathered to rank 0.
The two endpoints are the same mechanism, so this is one executor with
--boot-ranks-per-batchas its only new rank knob:K = 1→G = R: each rank its own group, a direct EF per batch (what PR-3shipped). Reproduced bit-for-bit as the degenerate case, so the standalone
user_boot/simulate_bootare unaffected.K = R→G = 1: one group of all ranks, a wheel per batch in sequence (thedevelopment checkpoint).
The pieces
BatchExecutor(confidence_intervals/bootsp/batch_executor.py) — ownsthe rank arithmetic and the collectives: each group's communicator (wheel /
xhat-evaluation), a leaders-only communicator (the cross-group
Gatherv),per-group batch share and rng seed, and
gather().boot_sp.py) — the empirical estimators route theirparallelism through the executor; the five repeated
Gathervblocks collapseinto
executor.gather, and a single_batch_optimal_valueis the one entryfor the batch optimal
L_b(EF best bound atK = 1, wheel outer bound atK > 1). The smoothed methods and the simulation prep keep theK = 1-onlypath via an optional
mpicomm.generic/boot_batch.py) —--boot-batch-config-fileis parsed as a file of
generic_cylindersflags and turned into a per-batchwheel (reusing the hub/spoke/decomp builders with injected sample-scenario
names); its outer decomposition bound is read back as
L_b.do_boot/configdrop theK != 1restriction, require thebatch config file, and retire the interim
--boot-solver-name/--boot-solver-options(the batch solver now comes from the file).register_generic_argsfactored out ofparse_args(pure refactor) so thebatch config file and the main CLI share one registration point.
Why the outer bound (design §9.4.1)
A batch's reported gap is its value at
xhat(an inner bound, solver-exact) minusits optimal. For the optimal the executor uses the wheel's outer
(decomposition) bound, not an incumbent — a MIP batch carries a bound slack even
at
K = 1, so inner−outer keeps the reported point estimate conservative — itnever understates the gap an exact solve would report.
The interval endpoints carry no such guarantee, and an earlier version of
this description claimed they did. Each endpoint combines the point estimate
with a width built from the spread of the batch gaps, and a bound slack
perturbs that spread without a sign; the pivotal methods (
Classical_quantile,Subsampling,Extended) reflect the bootstrap quantiles about the pointestimate, so a batch slack can move both of their endpoints down. All the
methods do converge to the exact-solve CI as the batch gap tightens, so
--rel-gapin the batch config file is the lever when the interval matters.Validation
End to end on the
schultz_dataMIP, both endpoints reproduce the design's mathexactly: the
G = 1checkpoint (np=2, K=2) andG > 1(np=4, K=2) both give thesame value at
xhatas theK = 1EF path (the xhat-evaluation isK-invariant), while their Lagrangian/subgradient outer bound sits at or below the
EF optimum — so the reported gap is conservative.
mpisppy/tests/test_boot_generic.py::test_do_boot_g1_wheel_matches_efencodesthis self-validatingly (no magic numbers), gated on
n_proc >= 2so it runsunder the existing
mpiexec -np 2 test_boot_genericinvocation — no coverageharness change.
mpiexec -np 233 passed/ 2 skipped across the three MPI-run suites (incl. the G=1 wheel test).
ruffclean, headers OK, docs build with no warnings.Review fixes carried by this branch
A review of the whole stack against Chen & Woodruff (2023, 2024) and the
certified-bounds notes produced seven findings. Each fix sits on the lowest
branch whose code it corrects, so most arrive here by inheritance:
bootstrap, sample variances (
ddof=1) in smoothed bagging, and amaximization guard in
solve_routine.z*kept as theincumbent (with a convergence check), and a consistent fallback when the
solver proves no outer bound.
batches:
do_bootprobes one scenario for its objective sense (aK > 1batch is a wheel, so
solve_routine's check never sees it),_best_bound'sdocstring names its
boot_batchsibling and why that one raises where thisone falls back, and the relocated rst section regains the corrected wording.
Runnable demo
examples/bootsp/schultz_data/schultz_data_boot_cylinders.bash— 6 ranks findxhattogether, then re-form into two groups of three that solve the bootstrapbatches concurrently, each batch by a subgradient wheel (config in the committed
schultz_wheel_batch.txt). Runs in ~4 s with any MIP/MIQP solver; the log showsthe two concurrent 3-rank groups.
🤖 Generated with Claude Code