simjecture 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- conjecture_solver/__init__.py +10 -0
- conjecture_solver/__main__.py +4 -0
- conjecture_solver/action_handlers.py +459 -0
- conjecture_solver/adapters/__init__.py +7 -0
- conjecture_solver/adapters/base.py +96 -0
- conjecture_solver/adapters/fake.py +155 -0
- conjecture_solver/adapters/pic.py +196 -0
- conjecture_solver/adapters/warpx.py +1375 -0
- conjecture_solver/autonomous_research.py +1071 -0
- conjecture_solver/benchmarks/__init__.py +6 -0
- conjecture_solver/benchmarks/electrostatic_pic.py +547 -0
- conjecture_solver/benchmarks/kinetic_sufficiency.py +318 -0
- conjecture_solver/builtin_skills/warpx/SKILL.md +151 -0
- conjecture_solver/builtin_skills/warpx/agents/openai.yaml +4 -0
- conjecture_solver/builtin_skills/warpx/examples/implicit_em_smoke.py +74 -0
- conjecture_solver/builtin_skills/warpx/examples/minimal_smoke.py +54 -0
- conjecture_solver/builtin_skills/warpx/examples/openpmd_field_smoke.py +87 -0
- conjecture_solver/builtin_skills/warpx/manifest.json +11 -0
- conjecture_solver/builtin_skills/warpx/references/2d-xz-commissioning.md +145 -0
- conjecture_solver/builtin_skills/warpx/references/cpu-launch-tuning.md +54 -0
- conjecture_solver/builtin_skills/warpx/references/diagnostics.md +139 -0
- conjecture_solver/builtin_skills/warpx/references/gpu-launch-tuning.md +79 -0
- conjecture_solver/builtin_skills/warpx/references/local-cuda-deployment.md +87 -0
- conjecture_solver/builtin_skills/warpx/references/numerical-risks.md +32 -0
- conjecture_solver/builtin_skills/warpx/references/picmi-interface.md +118 -0
- conjecture_solver/builtin_skills/warpx/references/resource-scaling.md +24 -0
- conjecture_solver/builtin_skills/warpx/references/time-integration.md +58 -0
- conjecture_solver/builtin_skills/warpx/scripts/benchmark_cpu_threads.py +207 -0
- conjecture_solver/builtin_skills/warpx/scripts/benchmark_gpu.py +242 -0
- conjecture_solver/builtin_skills/warpx/scripts/bootstrap_local_cuda.sh +82 -0
- conjecture_solver/builtin_skills/warpx/scripts/probe_local_cuda.py +110 -0
- conjecture_solver/builtin_skills/warpx/scripts/reduced_energy_budget.py +195 -0
- conjecture_solver/builtin_skills/warpx/scripts/run_local_cuda.sh +39 -0
- conjecture_solver/campaign.py +406 -0
- conjecture_solver/cli.py +963 -0
- conjecture_solver/confirmation.py +287 -0
- conjecture_solver/control.py +234 -0
- conjecture_solver/deployment.py +874 -0
- conjecture_solver/discovery.py +68 -0
- conjecture_solver/domains/__init__.py +40 -0
- conjecture_solver/domains/base.py +157 -0
- conjecture_solver/domains/kinetic_sufficiency.py +521 -0
- conjecture_solver/ledger.py +238 -0
- conjecture_solver/lifecycle.py +131 -0
- conjecture_solver/literature.py +494 -0
- conjecture_solver/llm.py +228 -0
- conjecture_solver/models.py +279 -0
- conjecture_solver/mvp_agent.py +3212 -0
- conjecture_solver/mvp_claims.py +1279 -0
- conjecture_solver/mvp_control.py +322 -0
- conjecture_solver/mvp_guidance.py +172 -0
- conjecture_solver/mvp_launch.py +1296 -0
- conjecture_solver/mvp_monitor.py +1487 -0
- conjecture_solver/mvp_skills.py +538 -0
- conjecture_solver/orchestration.py +578 -0
- conjecture_solver/outbox.py +526 -0
- conjecture_solver/parameters.py +394 -0
- conjecture_solver/proposals.py +427 -0
- conjecture_solver/research_tools.py +278 -0
- conjecture_solver/schema_export.py +246 -0
- conjecture_solver/search.py +1047 -0
- conjecture_solver/semantics.py +146 -0
- conjecture_solver/tui/__init__.py +7 -0
- conjecture_solver/tui/app.py +202 -0
- conjecture_solver/tui/claim_views.py +213 -0
- conjecture_solver/tui/screens.py +1014 -0
- conjecture_solver/warpx_analysis.py +184 -0
- conjecture_solver/warpx_campaign.py +148 -0
- conjecture_solver/warpx_confirmation.py +617 -0
- simjecture-0.1.0.dist-info/METADATA +400 -0
- simjecture-0.1.0.dist-info/RECORD +75 -0
- simjecture-0.1.0.dist-info/WHEEL +4 -0
- simjecture-0.1.0.dist-info/entry_points.txt +4 -0
- simjecture-0.1.0.dist-info/licenses/LICENSE +202 -0
- simjecture-0.1.0.dist-info/licenses/NOTICE +6 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# PICMI construction map
|
|
2
|
+
|
|
3
|
+
WarpX exposes the PICMI interface through `from pywarpx import picmi`. A usual
|
|
4
|
+
program constructs objects in this order:
|
|
5
|
+
|
|
6
|
+
1. A geometry-specific grid such as `Cartesian1DGrid`, `Cartesian2DGrid`,
|
|
7
|
+
`Cartesian3DGrid`, or `CylindricalGrid`, with field and particle boundary
|
|
8
|
+
conditions stated explicitly.
|
|
9
|
+
2. An electromagnetic or electrostatic solver compatible with that grid and
|
|
10
|
+
the intended model.
|
|
11
|
+
3. One or more `Species` objects with explicit charge/mass or a recognized
|
|
12
|
+
particle type, each connected to an initial distribution.
|
|
13
|
+
4. A particle layout for each species, such as `GriddedLayout` or
|
|
14
|
+
`PseudoRandomLayout`.
|
|
15
|
+
5. A `Simulation` carrying timestep or CFL choice, maximum steps, particle
|
|
16
|
+
shape, random-seed policy, and any WarpX-specific controls.
|
|
17
|
+
6. Applied fields, collisions, lasers, or other supported components only when
|
|
18
|
+
demanded by the proposed experiment.
|
|
19
|
+
7. Diagnostics added to the simulation before `simulation.step()`.
|
|
20
|
+
|
|
21
|
+
`simulation.write_input_file(file_name=...)` is useful for inspecting the
|
|
22
|
+
compiled native input without running the calculation. When representation is
|
|
23
|
+
scientifically important, also request `warpx_used_inputs_file` and inspect the
|
|
24
|
+
realized native controls rather than trusting the frontend program alone.
|
|
25
|
+
|
|
26
|
+
PICMI uses physical units for dimensional inputs. Check dimensional
|
|
27
|
+
conversions explicitly. Parser expressions, velocity/momentum conventions,
|
|
28
|
+
thermal spreads, boundary names, and dimension-dependent component ordering
|
|
29
|
+
are common sources of plausible but incorrect initial states.
|
|
30
|
+
|
|
31
|
+
Despite their names, `AnalyticDistribution(momentum_expressions=...)` and
|
|
32
|
+
`momentum_spread_expressions=...` take proper velocity, `gamma*v`, in metres
|
|
33
|
+
per second in this runtime. They do **not** take mechanical momentum `m*gamma*v`.
|
|
34
|
+
For a nonrelativistic drift, supply the drift velocity itself; never multiply it
|
|
35
|
+
by particle mass. Thermal spread expressions likewise use a velocity spread.
|
|
36
|
+
Confirm the generated `*_u*_mean_function` and `*_u*_std_function` entries in
|
|
37
|
+
the used-inputs record. Parser keyword arguments must actually occur in at
|
|
38
|
+
least one expression consumed by that object; unused keywords are rejected by
|
|
39
|
+
PICMI, so pass only the constants used by each distribution or field.
|
|
40
|
+
|
|
41
|
+
For the pinned 26.07 runtime, use the concrete names implemented by the
|
|
42
|
+
installed interface. Important multidimensional entries include
|
|
43
|
+
`Cartesian2DGrid`, `AnalyticDistribution`, `AnalyticInitialField`,
|
|
44
|
+
`ConstantAppliedField`, `AnalyticAppliedField`, and `CoulombCollisions`. Do not
|
|
45
|
+
invent generic aliases such as `Grid` or `BinaryCollision`. Wire the objects
|
|
46
|
+
with the exact methods `simulation.add_species(species, layout=layout)`,
|
|
47
|
+
`simulation.add_applied_field(field)`, and
|
|
48
|
+
`simulation.add_diagnostic(diagnostic)`. In WarpX 26.07,
|
|
49
|
+
`CoulombCollisions` is not a standard PICMI interaction: pass a list through
|
|
50
|
+
`picmi.Simulation(..., warpx_collisions=[collision])`; do not use
|
|
51
|
+
`simulation.add_interaction(collision)`, which routes through the standard
|
|
52
|
+
interaction path. Construct it as
|
|
53
|
+
`picmi.CoulombCollisions(name="...", species=[species_a, species_b], ...)` in
|
|
54
|
+
this runtime. Its constructor requires the single `species` collection;
|
|
55
|
+
`species1=...` and `species2=...` without that collection are rejected. Use
|
|
56
|
+
`CoulombLog`, `ndt_supercycle`, or `ndt_subcycle` with the exact capitalization
|
|
57
|
+
shown by the runtime. If `CoulombLog` is omitted, WarpX calculates it locally;
|
|
58
|
+
if it is prescribed, justify its physical range and measure the realized
|
|
59
|
+
collision effect.
|
|
60
|
+
|
|
61
|
+
For `AnalyticInitialField`, provide explicit expressions for all three magnetic
|
|
62
|
+
components and all three electric components, using `"0.0"` for intended
|
|
63
|
+
zeros. The installed constructor keyword names are exactly
|
|
64
|
+
`Ex_expression`, `Ey_expression`, `Ez_expression`, `Bx_expression`,
|
|
65
|
+
`By_expression`, and `Bz_expression`. Do not pass `Ex`/`Ey`/`Ez`/`Bx`/`By`/`Bz`
|
|
66
|
+
or `E_x`/`B_x`-style aliases; those are rejected as unexpected keywords before
|
|
67
|
+
WarpX initializes. `ConstantAppliedField` is different: it takes numeric
|
|
68
|
+
`Ex`/`Ey`/`Ez`/`Bx`/`By`/`Bz` (not `E1`/`E2`/`E3` and not `*_expression`).
|
|
69
|
+
Supplying only one analytic component can reach WarpX initialization and then
|
|
70
|
+
abort when another external-grid parser entry is absent. Inspect the native
|
|
71
|
+
input for `B_ext_grid_init_style = parse_b_ext_grid_function` (and the analogous
|
|
72
|
+
electric entry when used); particle-applied external fields are a different
|
|
73
|
+
mechanism.
|
|
74
|
+
|
|
75
|
+
For a spatially varying thermal spread in `AnalyticDistribution`, the pinned
|
|
76
|
+
WarpX subclass consumes `warpx_momentum_spread_expressions`, not the generic
|
|
77
|
+
`momentum_spread_expressions` argument inherited from `picmistandard`. The
|
|
78
|
+
generic argument can be accepted while disappearing from the realized native
|
|
79
|
+
input, leaving only the directed momentum parser. Use numeric-literal strings
|
|
80
|
+
for constant spreads, or make every parser symbol part of an expression path
|
|
81
|
+
that the installed constructor actually registers. Verify native
|
|
82
|
+
`momentum_distribution_type = "maxwellian"`, all three
|
|
83
|
+
`u*_std_function(x,y,z)` expressions, and their `/c` normalization. A custom
|
|
84
|
+
keyword used only by the generic spread expressions can be rejected as an
|
|
85
|
+
unexpected keyword. For a spatially uniform Gaussian, `rms_velocity` is a
|
|
86
|
+
simpler standard interface; still inspect `ux_th`, `uy_th`, and `uz_th`.
|
|
87
|
+
|
|
88
|
+
The pinned Cartesian PICMI grid accepts field boundary strings `periodic`,
|
|
89
|
+
`open`, `dirichlet`, `neumann`, and `absorbing_silver_mueller`, subject to
|
|
90
|
+
solver compatibility. Particle boundary strings are `periodic`, `absorbing`,
|
|
91
|
+
`reflect`, and `thermal`. These are numerical boundary behaviors, not sources.
|
|
92
|
+
If the physics needs continuing material or flux injection, author and
|
|
93
|
+
commission an explicit supported injection/source mechanism; do not relabel an
|
|
94
|
+
open or absorbing boundary as inflow.
|
|
95
|
+
|
|
96
|
+
Call `simulation.write_input_file(file_name="inputs")` before stepping and set
|
|
97
|
+
`warpx_used_inputs_file` on `Simulation` when the realized controls matter.
|
|
98
|
+
Inspect both records. Do not repeatedly introspect the runtime for the wiring
|
|
99
|
+
listed here.
|
|
100
|
+
|
|
101
|
+
`PseudoRandomLayout(seed=...)` prints a warning because WarpX 26.07 does not
|
|
102
|
+
honor a layout-local seed. Use the simulation/runtime seed controls, retain the
|
|
103
|
+
warning, and verify stochastic reproducibility rather than assuming the
|
|
104
|
+
requested layout seed took effect.
|
|
105
|
+
|
|
106
|
+
Before a 2D Cartesian calculation, read `2d-xz-commissioning.md` in full.
|
|
107
|
+
|
|
108
|
+
## WarpX 1D coordinate mapping
|
|
109
|
+
|
|
110
|
+
`Cartesian1DGrid` uses `z` as its sole physical spatial axis. When using
|
|
111
|
+
`ParticleListDistribution`, supply longitudinal positions through `z` and
|
|
112
|
+
longitudinal proper velocities through `uz`; `x`, `y`, `ux`, and `uy` remain
|
|
113
|
+
transverse components. The longitudinal electric-field diagnostic is the `z`
|
|
114
|
+
component of the `E` mesh. Verify the mapping in `warpx_used_inputs_file` and
|
|
115
|
+
particle diagnostics when commissioning a new loading method.
|
|
116
|
+
|
|
117
|
+
Use `read_skill` on the smoke example only to learn object wiring. Select all
|
|
118
|
+
scientific values from the active hypothesis and commissioning reasoning.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Resource scaling
|
|
2
|
+
|
|
3
|
+
Estimate cost before launching a calculation. For explicit PIC, particle work
|
|
4
|
+
typically grows with cells times macroparticles per cell times species times
|
|
5
|
+
steps. Field work grows with cells, components, solver iterations, and steps.
|
|
6
|
+
Diagnostics can dominate storage when full particles or frequent mesh dumps
|
|
7
|
+
are requested.
|
|
8
|
+
|
|
9
|
+
Start commissioning with the smallest case that exercises the intended
|
|
10
|
+
geometry, species, solver, boundaries, and diagnostic path. Increase one
|
|
11
|
+
relevant fidelity axis at a time. Keep heavy raw output sparse unless the
|
|
12
|
+
observable requires it; use reduced diagnostics for inexpensive monitoring,
|
|
13
|
+
but retain enough raw information to challenge the estimator.
|
|
14
|
+
|
|
15
|
+
The sandbox exposes two distinct instruments: `warpx-cpu-26.07` is a bounded
|
|
16
|
+
eight-thread OpenMP CPU profile, while `warpx-cuda-openpmd-26.07` is a
|
|
17
|
+
non-MPI, 2D CUDA profile with HDF5 openPMD output. Read `cpu-launch-tuning.md`
|
|
18
|
+
or `gpu-launch-tuning.md` before a long campaign. Do not assume the GPU is
|
|
19
|
+
faster: run a bounded commissioning case with the actual workload, then spend
|
|
20
|
+
its measured throughput on the fidelity axis that most limits the intended
|
|
21
|
+
inference. Large multidimensional, high-particle, or long-duration proposals
|
|
22
|
+
may still be unreachable even though WarpX supports them on other machines.
|
|
23
|
+
An honest instrument-unreachable result is preferable to silently weakening
|
|
24
|
+
the physical question.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Electromagnetic time integration
|
|
2
|
+
|
|
3
|
+
Select the evolve scheme from the physical timescales and observable. Do not
|
|
4
|
+
infer a timestep from a familiar dimensional value alone. Record `dt` in
|
|
5
|
+
seconds and normalized to every potentially limiting plasma, cyclotron,
|
|
6
|
+
collision, transit, growth, drive, and diagnostic timescale.
|
|
7
|
+
|
|
8
|
+
## Explicit electromagnetic PIC
|
|
9
|
+
|
|
10
|
+
Omitting `warpx_evolve_scheme` uses WarpX's explicit default. For an auditable
|
|
11
|
+
choice, construct `picmi.ExplicitEvolveScheme()` and pass it to
|
|
12
|
+
`picmi.Simulation(..., warpx_evolve_scheme=evolve_scheme)`.
|
|
13
|
+
|
|
14
|
+
For an explicit Yee update, enforce the multidimensional electromagnetic CFL
|
|
15
|
+
condition
|
|
16
|
+
|
|
17
|
+
`c*dt*sqrt(sum_i(1/dx_i**2)) < 1`.
|
|
18
|
+
|
|
19
|
+
For equal cells in two dimensions this gives `dt < dx/(c*sqrt(2))`. Also
|
|
20
|
+
resolve any plasma, gyro, collision, particle-crossing, or source timescale
|
|
21
|
+
that can affect the proposed observable. A CFL-passing step is not by itself
|
|
22
|
+
an accurate kinetic step.
|
|
23
|
+
|
|
24
|
+
## Implicit electromagnetic PIC
|
|
25
|
+
|
|
26
|
+
The pinned 26.07 PICMI runtime exposes:
|
|
27
|
+
|
|
28
|
+
- `picmi.ThetaImplicitEMEvolveScheme(nonlinear_solver=..., theta=...)`;
|
|
29
|
+
- `picmi.SemiImplicitEMEvolveScheme(nonlinear_solver=...)`;
|
|
30
|
+
- `picmi.NewtonNonlinearSolver(...)` and
|
|
31
|
+
`picmi.PicardNonlinearSolver(...)`; and
|
|
32
|
+
- `picmi.GMRESLinearSolver(...)` for a Newton solve when appropriate.
|
|
33
|
+
|
|
34
|
+
Pass the selected evolve object through
|
|
35
|
+
`picmi.Simulation(..., warpx_evolve_scheme=evolve_scheme)`. Require nonlinear
|
|
36
|
+
convergence and preserve the configured tolerance, iteration limit, iteration
|
|
37
|
+
history, and failure state. Inspect `warpx_used_inputs_file` for
|
|
38
|
+
`algo.evolve_scheme = theta_implicit_em` or `semi_implicit_em`, the intended
|
|
39
|
+
`implicit_evolve.nonlinear_solver`, and the exact `warpx.const_dt`.
|
|
40
|
+
|
|
41
|
+
Implicit advancement removes the explicit electromagnetic CFL condition as a
|
|
42
|
+
stability restriction; it does not remove accuracy restrictions. The step
|
|
43
|
+
must still resolve the dynamics used as evidence, particle motion relevant to
|
|
44
|
+
the estimator, collisions or forcing as implemented, diagnostic sampling, and
|
|
45
|
+
changes in the represented state. Qualify the chosen step against a smaller
|
|
46
|
+
`dt` while holding the physical case, duration, seed policy, and estimator
|
|
47
|
+
fixed. Reject a run whose nonlinear solve fails or whose result depends
|
|
48
|
+
materially on solver tolerances.
|
|
49
|
+
|
|
50
|
+
On the local 2D CUDA/openPMD 26.07 runtime, theta-implicit Picard has
|
|
51
|
+
repeatedly aborted with CUDA error 700 inside current deposition when a
|
|
52
|
+
loaded 2D electromagnetic plasma is advanced, even when the one-step implicit
|
|
53
|
+
wiring smoke succeeds. That is a scheme-specific solver-path failure, not an
|
|
54
|
+
openPMD/HDF5 failure. If the operator forbids implicit evolution or requires
|
|
55
|
+
explicit electromagnetic PIC, do not spend the campaign commissioning implicit
|
|
56
|
+
loaded-plasma cases. Use `ExplicitEvolveScheme` (or the
|
|
57
|
+
explicit default), enforce the multidimensional CFL condition, and treat
|
|
58
|
+
implicit only as a documented unavailable path.
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Benchmark one exact operator-supplied program argv across OpenMP settings."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import csv
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import statistics
|
|
12
|
+
import subprocess
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
EVOLVE_RE = re.compile(
|
|
17
|
+
r"Evolve time = ([0-9.eE+-]+) s;.*Avg\. per step = ([0-9.eE+-]+) s"
|
|
18
|
+
)
|
|
19
|
+
PROFILE_EVOLVE_RE = re.compile(
|
|
20
|
+
r"^WarpX::Evolve\(\)\s+1\s+([0-9.eE+-]+)", re.MULTILINE
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_args() -> argparse.Namespace:
|
|
25
|
+
parser = argparse.ArgumentParser()
|
|
26
|
+
parser.add_argument("--python", type=Path, required=True)
|
|
27
|
+
parser.add_argument("--program", type=Path, required=True)
|
|
28
|
+
parser.add_argument("--output", type=Path, required=True)
|
|
29
|
+
parser.add_argument("--threads", required=True)
|
|
30
|
+
parser.add_argument("--repeats", type=int, default=2)
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"--affinity", choices=("none", "close", "spread"), default="close"
|
|
33
|
+
)
|
|
34
|
+
parser.add_argument("--places", choices=("cores", "threads"), default="cores")
|
|
35
|
+
parser.add_argument("--required-path", action="append", required=True)
|
|
36
|
+
parser.add_argument("program_argv", nargs=argparse.REMAINDER)
|
|
37
|
+
args = parser.parse_args()
|
|
38
|
+
if args.program_argv[:1] == ["--"]:
|
|
39
|
+
args.program_argv = args.program_argv[1:]
|
|
40
|
+
return args
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def contained_paths(values: list[str]) -> tuple[Path, ...]:
|
|
44
|
+
paths = tuple(Path(value) for value in values)
|
|
45
|
+
for path in paths:
|
|
46
|
+
if path.is_absolute() or ".." in path.parts:
|
|
47
|
+
raise ValueError("--required-path must be a contained relative path")
|
|
48
|
+
return paths
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def amrex_capabilities(python: Path) -> dict[str, object]:
|
|
52
|
+
probe = subprocess.run(
|
|
53
|
+
[
|
|
54
|
+
str(python),
|
|
55
|
+
"-c",
|
|
56
|
+
(
|
|
57
|
+
"import json, amrex.space2d as a; "
|
|
58
|
+
"print(json.dumps({'have_mpi': bool(a.Config.have_mpi), "
|
|
59
|
+
"'have_omp': bool(a.Config.have_omp)}))"
|
|
60
|
+
),
|
|
61
|
+
],
|
|
62
|
+
check=True,
|
|
63
|
+
text=True,
|
|
64
|
+
capture_output=True,
|
|
65
|
+
)
|
|
66
|
+
return json.loads(probe.stdout)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def launch_environment(threads: int, affinity: str, places: str) -> dict[str, str]:
|
|
70
|
+
env = os.environ.copy()
|
|
71
|
+
env.update(
|
|
72
|
+
{
|
|
73
|
+
"OMP_NUM_THREADS": str(threads),
|
|
74
|
+
"OPENBLAS_NUM_THREADS": "1",
|
|
75
|
+
"MKL_NUM_THREADS": "1",
|
|
76
|
+
"NUMEXPR_NUM_THREADS": "1",
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
if affinity == "none":
|
|
80
|
+
env.pop("OMP_PROC_BIND", None)
|
|
81
|
+
env.pop("OMP_PLACES", None)
|
|
82
|
+
else:
|
|
83
|
+
env["OMP_PROC_BIND"] = affinity
|
|
84
|
+
env["OMP_PLACES"] = places
|
|
85
|
+
return env
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def require_outputs(run: Path, paths: tuple[Path, ...]) -> dict[str, int]:
|
|
89
|
+
observed: dict[str, int] = {}
|
|
90
|
+
for relative in paths:
|
|
91
|
+
target = run / relative
|
|
92
|
+
if not target.is_file() or target.stat().st_size == 0:
|
|
93
|
+
raise RuntimeError(f"required nonempty output is absent: {relative}")
|
|
94
|
+
observed[relative.as_posix()] = target.stat().st_size
|
|
95
|
+
return observed
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def timing_from_log(output: str) -> tuple[float | None, float | None]:
|
|
99
|
+
timings = EVOLVE_RE.findall(output)
|
|
100
|
+
if timings:
|
|
101
|
+
return tuple(float(value) for value in timings[-1]) # type: ignore[return-value]
|
|
102
|
+
profile = [float(value) for value in PROFILE_EVOLVE_RE.findall(output)]
|
|
103
|
+
return (max(profile), None) if profile else (None, None)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def run_one(
|
|
107
|
+
args: argparse.Namespace,
|
|
108
|
+
required: tuple[Path, ...],
|
|
109
|
+
threads: int,
|
|
110
|
+
repeat: int,
|
|
111
|
+
) -> dict[str, object]:
|
|
112
|
+
run = args.output / f"omp-{threads:03d}-{args.affinity}-{args.places}-r{repeat}"
|
|
113
|
+
run.mkdir(parents=True, exist_ok=False)
|
|
114
|
+
command = [str(args.python), str(args.program), *args.program_argv]
|
|
115
|
+
started = time.perf_counter()
|
|
116
|
+
completed = subprocess.run(
|
|
117
|
+
command,
|
|
118
|
+
cwd=run,
|
|
119
|
+
env=launch_environment(threads, args.affinity, args.places),
|
|
120
|
+
text=True,
|
|
121
|
+
stdout=subprocess.PIPE,
|
|
122
|
+
stderr=subprocess.STDOUT,
|
|
123
|
+
)
|
|
124
|
+
wall_s = time.perf_counter() - started
|
|
125
|
+
(run / "benchmark.log").write_text(completed.stdout)
|
|
126
|
+
if completed.returncode:
|
|
127
|
+
raise RuntimeError(
|
|
128
|
+
f"threads={threads} repeat={repeat} exited {completed.returncode}; "
|
|
129
|
+
f"see {run / 'benchmark.log'}"
|
|
130
|
+
)
|
|
131
|
+
outputs = require_outputs(run, required)
|
|
132
|
+
evolve_s, step_s = timing_from_log(completed.stdout)
|
|
133
|
+
return {
|
|
134
|
+
"threads": threads,
|
|
135
|
+
"affinity": args.affinity,
|
|
136
|
+
"places": args.places,
|
|
137
|
+
"repeat": repeat,
|
|
138
|
+
"wall_s": wall_s,
|
|
139
|
+
"warpx_evolve_s": evolve_s,
|
|
140
|
+
"warpx_step_s": step_s,
|
|
141
|
+
"required_outputs": json.dumps(outputs, sort_keys=True),
|
|
142
|
+
"run_dir": str(run),
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def main() -> int:
|
|
147
|
+
args = parse_args()
|
|
148
|
+
args.python = args.python.resolve()
|
|
149
|
+
args.program = args.program.resolve()
|
|
150
|
+
args.output = args.output.resolve()
|
|
151
|
+
required = contained_paths(args.required_path)
|
|
152
|
+
threads = [int(value) for value in args.threads.split(",")]
|
|
153
|
+
if not threads or any(value <= 0 for value in threads):
|
|
154
|
+
raise ValueError("--threads must contain positive integers")
|
|
155
|
+
if args.repeats <= 0:
|
|
156
|
+
raise ValueError("--repeats must be positive")
|
|
157
|
+
capabilities = amrex_capabilities(args.python)
|
|
158
|
+
mpi_world_size = int(
|
|
159
|
+
os.environ.get("OMPI_COMM_WORLD_SIZE")
|
|
160
|
+
or os.environ.get("PMI_SIZE")
|
|
161
|
+
or os.environ.get("MPI_LOCALNRANKS")
|
|
162
|
+
or "1"
|
|
163
|
+
)
|
|
164
|
+
if mpi_world_size > 1 and not capabilities["have_mpi"]:
|
|
165
|
+
raise RuntimeError(
|
|
166
|
+
"refusing MPI launch: loaded AMReX reports have_mpi=false"
|
|
167
|
+
)
|
|
168
|
+
args.output.mkdir(parents=True, exist_ok=False)
|
|
169
|
+
rows = [
|
|
170
|
+
run_one(args, required, count, repeat)
|
|
171
|
+
for count in threads
|
|
172
|
+
for repeat in range(1, args.repeats + 1)
|
|
173
|
+
]
|
|
174
|
+
with (args.output / "runs.csv").open("w", newline="") as stream:
|
|
175
|
+
writer = csv.DictWriter(stream, fieldnames=list(rows[0]))
|
|
176
|
+
writer.writeheader()
|
|
177
|
+
writer.writerows(rows)
|
|
178
|
+
summaries = [
|
|
179
|
+
{
|
|
180
|
+
"threads": count,
|
|
181
|
+
"median_wall_s": statistics.median(
|
|
182
|
+
float(row["wall_s"]) for row in rows if row["threads"] == count
|
|
183
|
+
),
|
|
184
|
+
}
|
|
185
|
+
for count in threads
|
|
186
|
+
]
|
|
187
|
+
result = {
|
|
188
|
+
"capabilities": capabilities,
|
|
189
|
+
"observed_mpi_world_size": mpi_world_size,
|
|
190
|
+
"program": str(args.program),
|
|
191
|
+
"program_argv": args.program_argv,
|
|
192
|
+
"required_paths": [path.as_posix() for path in required],
|
|
193
|
+
"repeats": args.repeats,
|
|
194
|
+
"affinity": args.affinity,
|
|
195
|
+
"places": args.places,
|
|
196
|
+
"summaries": summaries,
|
|
197
|
+
"best": min(summaries, key=lambda value: value["median_wall_s"]),
|
|
198
|
+
}
|
|
199
|
+
(args.output / "summary.json").write_text(
|
|
200
|
+
json.dumps(result, indent=2, sort_keys=True) + "\n"
|
|
201
|
+
)
|
|
202
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
203
|
+
return 0
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
if __name__ == "__main__":
|
|
207
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Benchmark exact operator-supplied program argv cases on the CUDA runtime."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import csv
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import statistics
|
|
12
|
+
import subprocess
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
EVOLVE_RE = re.compile(
|
|
18
|
+
r"Evolve time = ([0-9.eE+-]+) s;.*Avg\. per step = ([0-9.eE+-]+) s"
|
|
19
|
+
)
|
|
20
|
+
PROFILE_EVOLVE_RE = re.compile(
|
|
21
|
+
r"^WarpX::Evolve\(\)\s+1\s+([0-9.eE+-]+)", re.MULTILINE
|
|
22
|
+
)
|
|
23
|
+
LABEL_RE = re.compile(r"^[A-Za-z0-9_.-]+$")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def parse_args() -> argparse.Namespace:
|
|
27
|
+
parser = argparse.ArgumentParser()
|
|
28
|
+
parser.add_argument("--python", type=Path, required=True)
|
|
29
|
+
parser.add_argument("--program", type=Path, required=True)
|
|
30
|
+
parser.add_argument("--cases", type=Path, required=True)
|
|
31
|
+
parser.add_argument("--output", type=Path, required=True)
|
|
32
|
+
parser.add_argument("--repeats", type=int, default=2)
|
|
33
|
+
parser.add_argument("--required-path", action="append", required=True)
|
|
34
|
+
return parser.parse_args()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def contained_paths(values: list[str]) -> tuple[Path, ...]:
|
|
38
|
+
paths = tuple(Path(value) for value in values)
|
|
39
|
+
for path in paths:
|
|
40
|
+
if path.is_absolute() or ".." in path.parts:
|
|
41
|
+
raise ValueError("--required-path must be a contained relative path")
|
|
42
|
+
return paths
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def read_cases(path: Path) -> tuple[dict[str, Any], ...]:
|
|
46
|
+
payload = json.loads(path.read_text())
|
|
47
|
+
cases = payload.get("cases") if isinstance(payload, dict) else None
|
|
48
|
+
if not isinstance(cases, list) or not cases:
|
|
49
|
+
raise ValueError("cases file must contain a nonempty 'cases' list")
|
|
50
|
+
result = []
|
|
51
|
+
labels = set()
|
|
52
|
+
for item in cases:
|
|
53
|
+
if not isinstance(item, dict) or set(item) != {"label", "argv"}:
|
|
54
|
+
raise ValueError("each case must contain exactly 'label' and 'argv'")
|
|
55
|
+
label = item["label"]
|
|
56
|
+
argv = item["argv"]
|
|
57
|
+
if not isinstance(label, str) or not LABEL_RE.fullmatch(label):
|
|
58
|
+
raise ValueError("case labels may contain only letters, digits, ._- ")
|
|
59
|
+
if label in labels:
|
|
60
|
+
raise ValueError(f"duplicate case label: {label}")
|
|
61
|
+
if not isinstance(argv, list) or not all(isinstance(v, str) for v in argv):
|
|
62
|
+
raise ValueError(f"case {label!r} argv must be a list of strings")
|
|
63
|
+
labels.add(label)
|
|
64
|
+
result.append({"label": label, "argv": argv})
|
|
65
|
+
return tuple(result)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def amrex_capabilities(python: Path) -> dict[str, object]:
|
|
69
|
+
code = (
|
|
70
|
+
"import json, amrex.space2d as a; "
|
|
71
|
+
"print(json.dumps({'have_mpi': bool(a.Config.have_mpi), "
|
|
72
|
+
"'have_omp': bool(a.Config.have_omp), "
|
|
73
|
+
"'have_gpu': bool(a.Config.have_gpu), "
|
|
74
|
+
"'gpu_backend': str(a.Config.gpu_backend)}))"
|
|
75
|
+
)
|
|
76
|
+
probe = subprocess.run(
|
|
77
|
+
[str(python), "-c", code], check=True, text=True, capture_output=True
|
|
78
|
+
)
|
|
79
|
+
return json.loads(probe.stdout)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def gpu_identity() -> dict[str, str]:
|
|
83
|
+
query = subprocess.run(
|
|
84
|
+
[
|
|
85
|
+
"nvidia-smi",
|
|
86
|
+
"--query-gpu=name,memory.total,compute_cap,driver_version",
|
|
87
|
+
"--format=csv,noheader,nounits",
|
|
88
|
+
],
|
|
89
|
+
check=True,
|
|
90
|
+
text=True,
|
|
91
|
+
capture_output=True,
|
|
92
|
+
)
|
|
93
|
+
name, memory_mib, compute_cap, driver = (
|
|
94
|
+
value.strip() for value in query.stdout.splitlines()[0].split(",")
|
|
95
|
+
)
|
|
96
|
+
return {
|
|
97
|
+
"name": name,
|
|
98
|
+
"memory_mib": memory_mib,
|
|
99
|
+
"compute_capability": compute_cap,
|
|
100
|
+
"driver_version": driver,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def capability_environment(python: Path) -> dict[str, str]:
|
|
105
|
+
env = os.environ.copy()
|
|
106
|
+
python_site = subprocess.run(
|
|
107
|
+
[str(python), "-c", "import sysconfig; print(sysconfig.get_paths()['purelib'])"],
|
|
108
|
+
check=True,
|
|
109
|
+
text=True,
|
|
110
|
+
capture_output=True,
|
|
111
|
+
).stdout.strip()
|
|
112
|
+
bundled_site = str(Path(python_site) / "pywarpx" / "site-packages")
|
|
113
|
+
env["PYTHONPATH"] = os.pathsep.join(
|
|
114
|
+
value for value in (bundled_site, python_site, env.get("PYTHONPATH", "")) if value
|
|
115
|
+
)
|
|
116
|
+
env["WARPX_PYTHON_SITE"] = python_site
|
|
117
|
+
env.update(
|
|
118
|
+
{
|
|
119
|
+
"OMP_NUM_THREADS": "1",
|
|
120
|
+
"OPENBLAS_NUM_THREADS": "1",
|
|
121
|
+
"MKL_NUM_THREADS": "1",
|
|
122
|
+
"NUMEXPR_NUM_THREADS": "1",
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
return env
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def require_outputs(run: Path, paths: tuple[Path, ...]) -> dict[str, int]:
|
|
129
|
+
observed: dict[str, int] = {}
|
|
130
|
+
for relative in paths:
|
|
131
|
+
target = run / relative
|
|
132
|
+
if not target.is_file() or target.stat().st_size == 0:
|
|
133
|
+
raise RuntimeError(f"required nonempty output is absent: {relative}")
|
|
134
|
+
observed[relative.as_posix()] = target.stat().st_size
|
|
135
|
+
return observed
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def timing_from_log(output: str) -> tuple[float | None, float | None]:
|
|
139
|
+
timings = EVOLVE_RE.findall(output)
|
|
140
|
+
if timings:
|
|
141
|
+
return tuple(float(value) for value in timings[-1]) # type: ignore[return-value]
|
|
142
|
+
profile = [float(value) for value in PROFILE_EVOLVE_RE.findall(output)]
|
|
143
|
+
return (max(profile), None) if profile else (None, None)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def run_one(
|
|
147
|
+
args: argparse.Namespace,
|
|
148
|
+
case: dict[str, Any],
|
|
149
|
+
required: tuple[Path, ...],
|
|
150
|
+
repeat: int,
|
|
151
|
+
env: dict[str, str],
|
|
152
|
+
) -> dict[str, object]:
|
|
153
|
+
run = args.output / f"{case['label']}-r{repeat}"
|
|
154
|
+
run.mkdir(parents=True, exist_ok=False)
|
|
155
|
+
command = [str(args.python), str(args.program), *case["argv"]]
|
|
156
|
+
started = time.perf_counter()
|
|
157
|
+
completed = subprocess.run(
|
|
158
|
+
command,
|
|
159
|
+
cwd=run,
|
|
160
|
+
env=env,
|
|
161
|
+
text=True,
|
|
162
|
+
stdout=subprocess.PIPE,
|
|
163
|
+
stderr=subprocess.STDOUT,
|
|
164
|
+
)
|
|
165
|
+
wall_s = time.perf_counter() - started
|
|
166
|
+
(run / "benchmark.log").write_text(completed.stdout)
|
|
167
|
+
if completed.returncode:
|
|
168
|
+
raise RuntimeError(
|
|
169
|
+
f"case={case['label']} repeat={repeat} exited {completed.returncode}; "
|
|
170
|
+
f"see {run / 'benchmark.log'}"
|
|
171
|
+
)
|
|
172
|
+
outputs = require_outputs(run, required)
|
|
173
|
+
evolve_s, step_s = timing_from_log(completed.stdout)
|
|
174
|
+
return {
|
|
175
|
+
"label": case["label"],
|
|
176
|
+
"repeat": repeat,
|
|
177
|
+
"wall_s": wall_s,
|
|
178
|
+
"warpx_evolve_s": evolve_s,
|
|
179
|
+
"warpx_step_s": step_s,
|
|
180
|
+
"argv": json.dumps(case["argv"]),
|
|
181
|
+
"required_outputs": json.dumps(outputs, sort_keys=True),
|
|
182
|
+
"run_dir": str(run),
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def main() -> int:
|
|
187
|
+
args = parse_args()
|
|
188
|
+
args.python = args.python.resolve()
|
|
189
|
+
args.program = args.program.resolve()
|
|
190
|
+
args.output = args.output.resolve()
|
|
191
|
+
cases = read_cases(args.cases.resolve())
|
|
192
|
+
required = contained_paths(args.required_path)
|
|
193
|
+
if args.repeats <= 0:
|
|
194
|
+
raise ValueError("--repeats must be positive")
|
|
195
|
+
capabilities = amrex_capabilities(args.python)
|
|
196
|
+
if not capabilities["have_gpu"]:
|
|
197
|
+
raise RuntimeError("refusing benchmark: loaded AMReX reports have_gpu=false")
|
|
198
|
+
if str(capabilities["gpu_backend"]).upper() != "CUDA":
|
|
199
|
+
raise RuntimeError(
|
|
200
|
+
f"this harness expects CUDA, got {capabilities['gpu_backend']}"
|
|
201
|
+
)
|
|
202
|
+
args.output.mkdir(parents=True, exist_ok=False)
|
|
203
|
+
env = capability_environment(args.python)
|
|
204
|
+
rows = [
|
|
205
|
+
run_one(args, case, required, repeat, env)
|
|
206
|
+
for case in cases
|
|
207
|
+
for repeat in range(1, args.repeats + 1)
|
|
208
|
+
]
|
|
209
|
+
with (args.output / "runs.csv").open("w", newline="") as stream:
|
|
210
|
+
writer = csv.DictWriter(stream, fieldnames=list(rows[0]))
|
|
211
|
+
writer.writeheader()
|
|
212
|
+
writer.writerows(rows)
|
|
213
|
+
summaries = [
|
|
214
|
+
{
|
|
215
|
+
"label": case["label"],
|
|
216
|
+
"argv": case["argv"],
|
|
217
|
+
"median_wall_s": statistics.median(
|
|
218
|
+
float(row["wall_s"])
|
|
219
|
+
for row in rows
|
|
220
|
+
if row["label"] == case["label"]
|
|
221
|
+
),
|
|
222
|
+
}
|
|
223
|
+
for case in cases
|
|
224
|
+
]
|
|
225
|
+
result = {
|
|
226
|
+
"capabilities": capabilities,
|
|
227
|
+
"gpu": gpu_identity(),
|
|
228
|
+
"program": str(args.program),
|
|
229
|
+
"required_paths": [path.as_posix() for path in required],
|
|
230
|
+
"repeats": args.repeats,
|
|
231
|
+
"summaries": summaries,
|
|
232
|
+
"best": min(summaries, key=lambda value: value["median_wall_s"]),
|
|
233
|
+
}
|
|
234
|
+
(args.output / "summary.json").write_text(
|
|
235
|
+
json.dumps(result, indent=2, sort_keys=True) + "\n"
|
|
236
|
+
)
|
|
237
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
238
|
+
return 0
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
if __name__ == "__main__":
|
|
242
|
+
raise SystemExit(main())
|