Skip to content

perf(cam): compile the CAM driver — jit + lax.scan (plan 027) - #80

Open
aliakherati wants to merge 3 commits into
feat/cam-driverfrom
feat/cam-driver-jit
Open

perf(cam): compile the CAM driver — jit + lax.scan (plan 027)#80
aliakherati wants to merge 3 commits into
feat/cam-driverfrom
feat/cam-driver-jit

Conversation

@aliakherati

Copy link
Copy Markdown
Contributor

Stacked on #74 (base = feat/cam-driver) while merges to main are held. Phase-B optimization per ADR-004 — plan 025 established correctness; this PR only compiles it. Plan: docs/plans/027-cam-driver-jit.md.

What

cam_run_step / cam_run_timesteps become thin unjitted wrappers — resolving topology=None outside the trace (get_topology() deliberately raises inside a jit trace) and stabilising the scan-carry pytree (qaerwat pre-populated with the pbuf's zeros default) — over jitted inners:

  • the substep loop and the step loop are lax.scan; the per-substep reseed flag and the per-step first_step flag ride the scan xs as traced booleans, so the box-reference reseed mode and production CAM's lagged mode share one compiled body;
  • statics per ADR-020's split: topology (frozen/hashable — the feat(topology): make the jit-staleness hazard impossible to hit silently #65 staleness rule), strat, n_substeps (scan length), the do_* toggles, the two compat flags. so2_to_h2so4_rate and the state are traced: a rate sweep reuses one compilation.

No physics or API change.

Verification

  • Every existing test passes unchanged, including all four end-to-end G5 parity trajectories (per-tracer at the reference's print floor, total sulfur 1e-13-gated).
  • The scan trajectory is bit-identical to repeated cam_run_step calls (assert_array_equal, not allclose) — same jitted body either way.
  • Cache semantics locked by test: a new rate VALUE must not retrace; a new n_substeps must.
  • Full suite: 272 passed.

Measured

G5 workload (120 steps × 16 substeps, strat, cam_mam4):

wall time
eager (before) 81.6 s
jitted, cold (compile + run) ~1.0 s
jitted, warm 0.02 s (~4000×)

The trajectory scan traces the step body once regardless of n_steps.

Docs updated in-PR per rule 5: plan 027 (with results), PROGRESS.md, and the FEATURES.md driver row.

Phase-B (ADR-004) for the plan-025 driver, mirroring the M6 PR-J1/J2
pattern from the E3SM side:

- cam_run_step / cam_run_timesteps become thin UNJITTED wrappers whose
  only jobs are resolving topology=None outside the trace
  (get_topology() deliberately raises inside a jit trace) and
  stabilising the carry pytree (qaerwat pre-populated with the pbuf's
  zeros default), over jitted inners.
- The substep loop and the step loop are lax.scan. The per-substep
  reseed flag and the per-step first_step flag ride the scan xs as
  traced booleans, so the box-reference reseed mode and production
  CAM's lagged mode share one compiled body.
- Statics per ADR-020's split: topology (frozen/hashable, the #65
  staleness rule), strat, n_substeps (scan length), the do_* toggles,
  and the two compat flags. so2_to_h2so4_rate and the whole state are
  traced operands: a rate sweep reuses one compilation — locked by
  test_rate_is_traced_and_substeps_are_static, alongside the inverse
  assertion that a new n_substeps DOES retrace.
- The scan trajectory is BIT-IDENTICAL to repeated cam_run_step calls
  (same jitted body; assert_array_equal, not allclose).

No physics or API change; every existing test passes unchanged,
including all four end-to-end G5 parity bars.

Measured on the G5 workload (120 steps x 16 substeps, strat,
cam_mam4): eager 81.6 s -> ~1.0 s cold (compile + run) / 0.02 s warm —
~4000x. The trajectory scan traces the step body once regardless of
n_steps.

Full suite green (272).
@aliakherati aliakherati self-assigned this Aug 27, 2026
Six figures under docs/figures (cam_strat_rh_*) regenerated end-to-end
by scripts/cam_rh_scenarios.py — exploratory scenario science, not
validation (no Fortran reference attached):

- 6-h strong-forcing sweep (SO2 = 0.1 ppmv, volcanic-plume scale):
  the classic nucleation-growth banana; number and mass are
  RH-insensitive because the regime is production-limited.
- 12-h weak-forcing experiments (H2SO4 = 1e7 molec/cm3) isolating the
  RH -> water uptake -> wet diameter -> condensation-sink chain:
  burst (no SO2) shows RH-ordered gas-depletion floors spanning 3.5
  decades; background (SO2 = 1e-10 vmr) shows a ~12% number spread
  with MORE particles at LOW RH (the weaker sink leaves gas standing
  for nucleation); condensation-only (nucleation off) fans the wet
  aitken diameter 31.5 -> 36.2 nm at identical dry size with 35%
  lower steady gas at RH 50%.

Also the empirical answer to 'is 1e7/cm3 enough': at 232 K it
nucleates completely within one 30 s step at any RH, so RH-sensitive
NUMBER requires the sustained weak-production regime.

Figure conventions: per-mode categorical colors in fixed order across
all figures; ordered dimensions (time, RH) on sequential ramps;
per-mode breakouts everywhere (no collapsed mode fields).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant