Write every model as a recipe; all seventeen compile into one shared library - #16
Merged
Conversation
Each converted problem gains `<name>_recipe` — its structure, with the size
left open — and `<name>_args`, the values that close it; `<name>_model`
becomes the two composed, so there is one definition rather than two that can
drift. Every model is unchanged: at the sizes the test suite itself uses for
callback comparison, all ten agree with the previous constructors on
dimensions, bounds, starting point, `minimize`, objective, gradient,
constraints, and the Jacobian and Hessian including COO order.
Converted: camshape, rocket, steering, channel, gasoil, catmix, methanol,
marine, pinene, chain.
What the size no longer being a number required:
* Scalars derived from it stay in the recipe as deferred expressions --
`h = tf/nh`, `d_theta = 2pi/(5(n+1))` -- including under exponentiation
(`h^(k-1)`) and in bounds (`lcon = -alpha*d_theta`). Written `one(T)/nh`
rather than `T(1)/T(nh)`: a constructor applied to a placeholder fails
where arithmetic defers.
* Uniform bounds and starts written as arrays sized by nh become scalars.
catmix's `lvar = zeros(T, nh, nc)` is `zero(T)`, which says the same thing
without depending on the size at all.
* Genuinely patterned data -- comprehensions over the size -- moves to
`<name>_args`, bundled as one named tuple rather than many arguments, so
the recipe reads `d.con1_itr` where the original read `con1_itr` and the
diff stays reviewable through dense collocation arithmetic.
* Rows indexing the last point become generators over `nh:nh`; where a fixed
set rides along, a two-variable generator; where coefficients ride along,
the index travels in the tuple with them (channel's bc3/bc4).
methanol keeps scalar starts on purpose: it builds `v0` through three loops and
then overwrites it wholesale with 0.001, so nothing size-dependent survives.
robot and glider are NOT converted. Both use `@add_expr`, which stores its body
as a closure; inside a model-building function that closure captures the
`Variable`s, whose types carry the placeholder, and `instantiate` cannot reach
inside a closure to resolve them. ExaModels refuses such a core rather than
returning a half-instantiated model. Converting them would need either that
fixed upstream or the expressions inlined by hand, which would change the
sparsity pattern.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bearing, torsion, minsurf
All seventeen problems the suite compares callbacks for are now split into
`<name>_recipe` (structure, size open) and `<name>_args` (the values that
close it), with `<name>_model` the two composed. Every one is identical to
the previous constructor at the sizes the suite itself uses, on dimensions,
bounds, starting point, `minimize`, objective, gradient, constraints, and the
Jacobian and Hessian including COO order.
Notes on the ones that were not mechanical:
* elec draws its starting distribution from a seed. That is data: the draw
and the seed both live in `elec_args`, so the recipe holds no randomness
and one structure serves every seed.
* robot and glider used `@add_expr`, whose body is stored as a closure that
captures the `Variable`s -- placeholders end up in the closure's type,
where `instantiate` cannot reach them. Since `@add_expr` adds no variables
and no constraints, only a name, a local helper splices the identical tree
at each point of use. glider's eight subexpressions chain through each
other exactly as before.
* bearing, torsion and minsurf take two sizes, so they carry three
placeholders: the two sizes and the data.
* minsurf's interior block is indexed by `Int(floor(0.25/hx))`; a constructor
applied to a placeholder fails, so those ranges are data -- the iteration
set itself, not merely the values in it.
Offloading moves values, not arithmetic. bearing first folded `e*sin(x)` into
one coefficient, which re-associates `(-hx*hy*e)*sin(x)` and moved twelve
gradient entries by one ulp; `sin(x)` alone is passed and `e` stays a factor in
the expression. A tolerance-based comparison would not have shown it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Moving derived data into `_args` kept the models identical and made all but
one of them uncompilable: `P_new` instantiates from integers, so a recipe
whose arguments carry tables is refused. The data now stays where the
recipe can reach it -- a deferred call to a named package function,
`d = ArgNode1(COPSBenchmark.foo_data, nh)` (ArgNode2 for the two-size
models), with `_args` returning only the sizes. `_recipe`/`_args`/`_model`
keep their meaning; only where the data is computed moves.
Because those functions now run inside the compiled library, they inherit
`--trim=safe`'s constraints, and two idioms that are fine under the JIT do
not survive it:
* `T[...]` where `T` is the keyword argument is a runtime-typed
comprehension -- `_array_for(T::Type, ...)` cannot resolve. Twenty
sites now use a concrete element type; the core converts on append.
* elec's seed is baked via `Base.Fix2(elec_data, seed)`, keeping every
type in the core named and making the library deterministic per seed.
robot and glider return to `@add_expr`, which stores a node rather than a
closure as of the ExaModels change this branch builds against.
All 20 harness cases remain identical to main's constructors. Compiled:
each model individually, then all seventeen into one shared library
(16.2 MB, 0 verifier errors), every model exact against its in-Julia
counterpart at sizes the library was never compiled at.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Checks what `*_model` cannot show now that it is defined as the composition: the core declares the arity its `*_args` supplies and its variable count is an expression rather than a number; and it is not consumed by being used — one core instantiates at two sizes and again at the first, each time equal to what `*_model` builds at that size on dimensions, starting point, objective and gradient. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hmark
Sungho's ruling on the Float32 finding: the `*_data` functions regain main's
T-generic semantics rather than declaring Float64 canonical. T travels as a
leading `::Val{T}` argument carried inside the deferred node — a bare
`::Type{T}` stores as an abstract `DataType` field and the deferred call
stays dynamic under `--trim=safe`; the `Val` singleton carries the type in
its own type, and the where-clause specialization makes the `T[...]`
comprehensions static. The `itau` integer index maps are computed in T
again, exactly as on main: measured identical to main at Float32 across all
eleven T-threaded models, structure and values, and identical at Float64
across all twenty harness cases. Data functions whose main-branch bodies
were Float64 arithmetic (bearing, chain, elec, minsurf, polygon) keep
size-only signatures — threading T through them would invent a dependency
main never had.
Style parity with LuksanVlcekBenchmark: `*_args` signatures carry only the
sizes they consume (T flows through the recipe; elec's seed is baked there);
the deferred-callable rule is stated in the extension entry file in the same
wording; the test pin moves to madsuite-org's #308 branch with its repoint
condition in the comment.
Known regression, not ours: the #308 tip currently fails `--trim=safe` on
cores carrying a deferred collect over a product iterator with a symbolic
factor — every 2-D COPS model. Verified against an earlier ExaModels
lineage with identical model-facing content: gasoil compiles and matches
exactly at sizes the library never saw. CI on this PR stays red on the
compile-adjacent legs until the branch fix lands.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
compile_all is a method on ExaModelsCompiler's generic function, implemented here in an extension triggered by ExaModelsCompiler as a weakdep -- so a benchmark package never acquires a compiler toolchain by being loaded. The seventeen problems are derived from the package's own surface via the *_recipe names rather than hand-listed, so a model added to src/ is compiled without editing the extension. Instantiation spec belongs to the model: the three grid problems take (nx, ny) and the rest a single size. only/exclude go through ExaModelsCompiler.select, which validates names against what the package actually provides. The test pin names a SHA rather than a branch: a branch rev is not reproducible under julia-actions/cache, since the job restores ~/.julia from an earlier run and can reuse that clone. Verified against ExaModels main a9fbf9cc: all 17 models compile into one library (16.07 MB, 0 verifier errors); 16 of 17 read back exact at sizes never compiled for. steering fails with 'steering_block returned nonzero status 2', an upstream metadata fault tracked on the ExaModels side, not a model defect -- the model itself builds and solves.
compile_all had no test at all: the provider's whole AOT surface -- which
problems it offers, what each is closed with, and the selection contract --
was resting on manual runs.
Most of it is testable without invoking a compiler, because select validates
names before compile_library is reached, so the error paths exercise
compile_all itself rather than a stand-in and cost nothing:
- the derived problem list covers the package. The extension builds its
models from the *_recipe names rather than a hand-written list precisely so
it cannot drift as models are added; this is what makes that a fact rather
than an intention.
- every (recipe, args...) pair compile_all would hand the compiler closes
into a model. This is what breaks when a recipe and its *_args disagree.
- an unknown name is refused, rather than silently yielding a library missing
the model the caller asked for; so is a selection that excludes everything,
and so is a package that implements no method.
Then one real compile of a single small model, because a list that assembles is
not evidence that anything in it compiles. Bounded deliberately: if it proves
too slow for the runner it belongs in a separate job, and CI is what tells us.
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.
Every model is written as a recipe —
<name>_recipe(structure, sizes open),<name>_args(the integers that close it),<name>_model(the two composed).All seventeen compile ahead of time, individually and into one shared library
(16.2 MB, 0 verifier errors), every model exact against its in-Julia
counterpart at sizes the library was never compiled at — measured locally,
not in CI; no committed test compiles a library, so these numbers are
re-established only by re-running the local harness against the pinned
ExaModels.
Fourteen models instantiate from one integer;
bearing/minsurf/torsionfrom two through the builder surface;
elec's seed is baked into the recipevia
Base.Fix2on a named function, so one structure serves every seed.Nothing about the models changes
All twenty harness cases — the suite's own comparison sizes plus the PDE
family — are identical to
main's constructors on dimensions, bounds,starting point,
minimize, objective, gradient, constraints, and the Jacobianand Hessian including COO order (measured locally with a mutation-armed
harness; the in-CI testset is the recipe testset below plus the pre-existing
JuMP comparison). At Float32 the eleven T-dependent models are identical to
main in structure and values — the
itauindex maps are computed inTexactly as on main.
The two rules AOT compilability adds
Data derived from a size is reached from the recipe by a deferred call to a
named, package-owned function (the rule is stated in the extension entry
file):
d = ArgCall(COPSBenchmark.gasoil_data, (Val(T), nh)), with*_argsreturning only the sizes.
Trides asVal{T}— a bare::Type{T}stores asan abstract
DataTypefield and the deferred call stays dynamic under--trim=safe. And because those functions run inside the compiled library,comprehensions need concrete element types —
T[...]under awhere {T}specialization is static;
T[...]with a keywordTis not.Known regression (not this PR's)
The #308 branch tip currently fails
--trim=safeon cores carrying a deferredcollectover a product iterator with a symbolic factor — every 2-D COPSmodel. Verified against an earlier ExaModels lineage with identical
model-facing content: compiles and matches exactly. The test pin tracks the
#308 branch (repoint condition in the pin's comment); compile-adjacent CI legs
stay red until the branch fix lands.
Requires madsuite-org/ExaModels.jl#308.