Skip to content

Human review: compare the Codon port and RecastEngine's NumPy translation of the same routines #8

Description

@chenyueqi

Context

Branch recast-gate adds a module-level bit-exactness harness driven by RecastEngine (see test/recast/README.md). Running it on one routine produced a three-way result worth a human look:

comparison result
Codon port vs native Fortran bit_exact — 43200/43200 points, 0 ULP
engine's NumPy translation vs native Fortran bit_exact — 43200/43200 points, 0 ULP
engine's NumPy translation vs Codon port bit-identical, 43200 points across 120 output arrays

So for this routine there are now two independent modernizations that agree with the original to the last bit — one written by the LLM-agent pipeline in this repository, one produced mechanically by RecastEngine's rule-driven translator. They are very different code. What the machine cannot judge is which properties matter for CAM going forward, and that is the ask below.

The ask

@cnitlrt — could you read the two generated implementations side by side and give an opinion? Not a correctness review (the gate covers that) but a judgement call: what does each path give up, and where would you want which?

Pair 1 — the routine already gated

  • native Fortran: src/physics/cam/vertical_diffusion.F90, vertical_diffusion_ptend_core_native, lines 2125–2159
  • Codon port: src/physics/cam/vertical_diffusion_codon.py, vertical_diffusion_ptend_core_codon, from line 588 (body from line 613)
  • engine's NumPy: not committed (it is generated). To produce it:
pip install -e ../RecastEngine'[fortran,translate,verify]' -e ../recast-cesm
clang -c -fPIC -O1 -o test/recast/build/vd_callback_stubs.o test/recast/vd_callback_stubs.c
codon build --relocation-model=pic -release --lib \
    --linker-flags="$PWD/test/recast/build/vd_callback_stubs.o" \
    -o test/recast/build/libvertical_diffusion_codon.dylib \
    src/physics/cam/vertical_diffusion_codon.py
python test/recast/run_threeway.py
# then read:
#   test/recast/work-threeway/engine/candidate/vd_ptend_core_numpy.py

The visible differences on this routine:

Codon port engine's NumPy
loops explicit for k / for i / for m, 1-based, range(1, pver + 1) vectorized [:ncol, :] slices
addressing hand-linearized _idx2(i, k, pcols) / _idx3(...) (definition near line 326), column-major arithmetic written out NumPy multi-dimensional indexing, 0-based
out arguments written through pointers in place, no return value intent(out/inout) become a returned tuple
dimensions passed explicitly (pcols, pver, pcnst, psetcols) read from a generated constants module; shapes carry the rest
provenance none every statement carries # B00N <- L45-L45 back to the Fortran line

Both produce the same bits here. That is not automatic — it holds because this routine is elementwise with no reduction, so no summation order can differ. Which brings the interesting question:

Pair 2 — where the two paths would be expected to diverge

dadadj is the opposite shape: iterative, converging, numerically fragile.

  • native Fortran: src/physics/cam/dadadj.F90, dadadj_native, from line 122
  • Codon port: src/physics/cam/dadadj_codon.py, dadadj_codon, from line 29
  • engine's NumPy: RecastEngine currently refuses this routine — test/recast/run_gate.py gates the Codon port against Fortran (9600/9600 bit-exact), but the translator defers one block, reporting:
dadadj_native/B004: goto 80 is not a loop-exit pattern

The two F77 gotos in dadadj_nativego to 80 jumping to the terminator of the labelled do 80 i=1,ncol (a cycle) and the backward go to 50 (a retry with doubled zeps) — have no mechanical rewrite in the translator yet. Per the plan discussed, those rules will be added upstream in CESM-language-translator and retrieved from there rather than invented here.

Questions worth your judgement

  1. Readability / maintainability. If someone has to debug a wrong number in three years, which of the two would you rather be holding? Does the engine's block-marker provenance (# B004 <- L48-L48) earn its keep, or is _idx2(i, k, pcols) clearer about what the memory is doing?
  2. Where vectorization is a trap. The NumPy form is bit-identical for elementwise work. For which CAM routines would you expect it to stop being — reductions, iterative solvers, anything where FMA contraction or SIMD reassociation can bite? Two more pure candidates to look at if useful: macrop_driver.F90 / macrop_driver_detrain_init_shell_native (line 1704, Codon at macrop_driver_codon.py:3931) and physpkg.F90 / tphysbc_init_fields_native (line 7220).
  3. Whether a second path is worth having at all. The Codon path is the one that ships — it links into CAM and keeps the calling interface. The NumPy path is standalone and cannot. Is a mechanically-generated, provenance-carrying NumPy version useful to you as a cross-check on the Codon port, as documentation, or not at all?
  4. The goto rules. When you add the cycle / backward-retry rules to CESM-language-translator, do the shapes in dadadj_native generalize, or is that routine's control flow unusual enough to be worth its own note?

Notes

  • A survey of this repository's 32 Codon-ported files found 49 routines that are pure (no module state read or written, no calls) and therefore gateable with no setup, so this harness extends by pointing it at another name.
  • The gate's inputs are physically ranged but synthetic, not sampled from a model run. Both paths agreeing here means they agree on the sampled region, not that the region CAM actually visits was covered — captured dumps are the honest next step.
  • Nothing under src/ was modified on recast-gate; the branch adds only test/recast/.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

questionFurther information is requested

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions