Skip to content

ExaModelsC: emit the schema + builder ABI for structured recipes - #309

Merged
sshin23 merged 2 commits into
abi/multi-model-producerfrom
anvil/builder-emitter
Aug 13, 2026
Merged

ExaModelsC: emit the schema + builder ABI for structured recipes#309
sshin23 merged 2 commits into
abi/multi-model-producerfrom
anvil/builder-emitter

Conversation

@sshin23

@sshin23 sshin23 commented Aug 13, 2026

Copy link
Copy Markdown
Member

Answers the multi-argument question from the other direction: compile_library now compiles recipes whose examples are not a single integer — several values, floats, arrays, tables, or NamedTuples of these, exactly as ExaModel takes them:

c, sz, dat, tab = ExaCore(nargs = Val(3))
@add_var(c, x, sz; start = dat.v0, lvar = dat.lo)
@add_obj(c, t.w * (x[t.i] - t.s)^2 for t in tab)

compile_library("@struct", c, 4, (v0 = fill(0.5, 4), lo = fill(-10.0, 4)),
                [(i = 1, w = 2.0, s = 1.0)])

The emitted surface is the ABI v2 schema + builder both consumers already implement — no consumer changes anywhere. P_schema publishes the flattened field list (NamedTuple examples flatten into one field per key, bare values are arg1, arg2, ... by position), the typed setters take values by name, and P_new_from_data reassembles the ExaModel arguments. Builder models export no P_new; a one-key integer NamedTuple keeps the P_new fast path — the disjointness the consumers use to route a lone integer.

Consumption is the consumers' existing positional spelling, one value per field:

CNLPModel("@struct", 6, v0, lo, tab)                    # Julia
cnlpmodels.CModel("struct", 6, v0, lo, {"i": ..., "w": ..., "s": ...})  # Python

Two commits. The first is in ExaModels proper: every instantiate method took a bare vararg that is only splatted through, which Julia's passthrough heuristic leaves unspecialized — invisible under the JIT, but the first multi-argument AOT compile surfaced it as 57 --trim=safe verifier errors. Annotating the signatures with Vararg{Any,N} forces specialization; behavior is unchanged (ArgumentTest 158/158).

Builder storage is generated concretely from the example types, so --trim=safe sees no dynamic containers; examples must be Int64/Float64 exactly (the example's type IS the compiled storage's type), refused otherwise with the reason.

Suite: 192/192 — including a compiled two-surface library (builder + one-knob model in one file), consumption from both CNLPModels.jl and cnlpmodels (Python driving the builder with a columnar table), an Ipopt solve through a builder-instantiated model, and refusal tests for strings, loose numeric types, colliding field names, and shape mismatches.

Based on #308 (multi-model) — merge that first; this rides on its ModelSpec machinery.

🤖 Generated with Claude Code

sshin23 and others added 2 commits August 12, 2026 23:15
Every instantiate method takes its arguments as a bare vararg that is
only splatted through to child calls. Julia's passthrough heuristic
leaves such varargs unspecialized — harmless under the JIT, but the
map over a core's block tuples then carries a dynamic call that
juliac --trim=safe reports as unresolved (57 verifier errors on the
first multi-argument recipe ever compiled). Annotating every signature
with Vararg{Any,N} forces specialization on the concrete argument
types; behavior is unchanged. ArgumentTest: 158/158.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Recipes whose examples are not a single integer — several values,
floats, arrays, tables, or NamedTuples of these, exactly as ExaModel
takes them — now compile to the ABI v2 builder surface both consumers
already implement: P_schema publishes the flattened field list,
P_data_begin / P_set_* / P_data_ready take the values by name, and
P_new_from_data reassembles the ExaModel arguments. A NamedTuple
example flattens into one schema field per key; bare values are named
arg1, arg2, ... by position. Builder models export no P_new — the
consumers rely on that disjointness to route a lone integer — and a
one-key integer NamedTuple keeps the P_new fast path.

Builder storage is generated concretely from the example types (one
slot per scalar/array field and per table column), so --trim=safe sees
no dynamic containers; examples must be Int64/Float64 exactly, refused
otherwise with the reason. Probe failures now surface as ArgumentError
naming the model instead of a raw MethodError.

Suite: 192/192, including a compiled two-surface library (builder +
one-knob in one file), consumption from CNLPModels.jl and cnlpmodels
(Python driving the builder with a columnar table), and an Ipopt solve
through a builder-instantiated model.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Your PR requires formatting changes to meet the project's style guidelines.

Please run:

julia --project=@runic -e 'using Pkg; Pkg.add("Runic")'
julia --project=@runic -e "using Runic; exit(Runic.main(ARGS))" -- --fix <files>

(or git runic abi/multi-model-producer if you have the git wrapper installed)

Note: the full diff is omitted because it can exceed GitHub Actions input limits.

@sshin23
sshin23 merged commit 263da41 into abi/multi-model-producer Aug 13, 2026
16 of 17 checks passed
@sshin23
sshin23 deleted the anvil/builder-emitter branch August 13, 2026 03:18
@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Results


Relative timing: current / main  (values < 1.0 are improvements)

================================================================================
  backend-instance-param          |      obj     cons     grad      jac     hess
================================================================================
  CUDA-OPF-case1354               |    1.101    1.084    1.143    1.095    1.082
  CUDA-OPF-case14                 |    1.098    1.102    1.137    1.105    1.092
  CUDA-OPF-case30000              |    1.086    1.075    1.138    1.089    1.081
  CUDA-chain-10                   |    1.093    1.131    1.142    1.131    1.115
  CUDA-chain-100                  |    1.090    1.130    1.000    1.003    0.999
  CUDA-chain-1000                 |    0.965    0.996    0.998    0.999    1.001
  CUDA-elec-10                    |    0.988    1.009    1.006    1.001    1.012
  CUDA-elec-100                   |    1.005    0.998    1.008    1.008    1.005
  CUDA-elec-1000                  |    0.946    1.001    1.284    1.002   37.807
  CUDA-rosenrock-1000             |    1.017    1.115    1.148    1.108    1.093
  CUDA-rosenrock-10000            |    1.097    1.159    1.166    1.106    1.091
  CUDA-rosenrock-100000           |    1.109    1.267    1.160    1.107    0.665
--------------------------------------------------------------------------------
  AMDGPU-OPF-case1354             |    1.013    0.995    0.990    0.994    1.005
  AMDGPU-OPF-case14               |    0.980    1.005    1.059    1.008    1.035
  AMDGPU-OPF-case30000            |    1.038    0.997    0.947    0.986    0.992
  AMDGPU-chain-10                 |    0.994    1.003    0.987    1.000    1.015
  AMDGPU-chain-100                |    0.965    1.017    0.998    1.000    0.992
  AMDGPU-chain-1000               |    1.022    1.013    1.006    0.999    0.999
  AMDGPU-elec-10                  |    0.961    0.990    0.989    1.012    0.997
  AMDGPU-elec-100                 |    1.019    0.991    0.996    1.011    1.004
  AMDGPU-elec-1000                |    0.982    0.992    1.220    1.015    1.005
  AMDGPU-rosenrock-1000           |    1.019    0.942    1.010    0.982    0.989
  AMDGPU-rosenrock-10000          |    0.944    1.040    0.981    0.976    0.995
  AMDGPU-rosenrock-100000         |    1.018    0.994    1.000    0.967    0.712
--------------------------------------------------------------------------------
  oneAPI-OPF-case1354             |    0.926    0.917    1.071    0.696    1.167
  oneAPI-OPF-case14               |    3.157    0.800    0.776    1.164    1.042
  oneAPI-OPF-case30000            |    2.329    0.946    0.897    1.387    1.115
  oneAPI-chain-10                 |    0.927    1.116    0.822    1.390    0.985
  oneAPI-chain-100                |    0.944    1.247    1.049    1.088    1.108
  oneAPI-chain-1000               |    1.041    0.991    0.768    1.563    0.930
  oneAPI-elec-10                  |    1.058    1.128    0.852    0.973    1.078
  oneAPI-elec-100                 |    0.855    0.763    0.968    1.054    0.934
  oneAPI-elec-1000                |    1.038    1.030    0.939    0.948    0.893
  oneAPI-rosenrock-1000           |    1.013    0.887    1.766    0.968    4.391
  oneAPI-rosenrock-10000          |    0.426    0.635    0.927    0.898    0.995
  oneAPI-rosenrock-100000         |    0.393    0.928    0.971    1.189    0.934
--------------------------------------------------------------------------------
  nothing-OPF-case1354            |    1.032    0.931    1.003    1.169    0.932
  nothing-OPF-case14              |    1.064    1.311    1.127    1.558    1.001
  nothing-OPF-case30000           |    1.003    1.137    0.998    1.090    1.029
  nothing-chain-10                |    1.686    0.989    1.210    1.012    0.875
  nothing-chain-100               |    1.886    0.960    1.250    1.010    1.131
  nothing-chain-1000              |    0.896    0.946    1.000    1.016    0.978
  nothing-elec-10                 |    0.994    1.108    0.980    1.204    1.012
  nothing-elec-100                |    1.009    0.989    0.929    1.003    0.998
  nothing-elec-1000               |    1.011    1.175    0.990    0.997    1.013
  nothing-rosenrock-1000          |    1.001    0.755    1.337    1.018    1.267
  nothing-rosenrock-10000         |    0.999    1.169    1.246    1.014    1.006
  nothing-rosenrock-100000        |    1.010    0.986    1.015    1.004    2.741
================================================================================

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