Skip to content

jax → ONNX export via jax2onnx: jaxtrain now produces HSSM-consumable artifacts - #109

Open
AlexanderFengler wants to merge 1 commit into
feat/mlflow-self-describing-trainingfrom
feat/jax-onnx-export
Open

jax → ONNX export via jax2onnx: jaxtrain now produces HSSM-consumable artifacts#109
AlexanderFengler wants to merge 1 commit into
feat/mlflow-self-describing-trainingfrom
feat/jax-onnx-export

Conversation

@AlexanderFengler

Copy link
Copy Markdown
Member

What

PR-0.3, stacked on #108 (review only the top commit; retarget to main after #108 merges). Closes the gap that made jaxtrain a dead end for HSSM: the jax trainer saved flax parameter bytes that nothing downstream could convert — transform-onnx only reads torch state dicts, and HSSM consumes ONNX exclusively.

  • New lanfactory/onnx/jax_export.py (~150 lines incl. docs): wraps jax2onnx (pinned ==0.15.*) over the existing JaxMLPFactory.make_forward_partial restore path — minimal-carry, no bespoke tracer
  • New transform-jax-onnx CLI (file-based: network_config.pickle + *_train_state.jax.onnx)
  • jaxtrain gains --export-onnx/--no-export-onnx (default on): the trainer exports ONNX right after training and logs it to MLflow
  • Follows the ecosystem single-trial ONNX contract: traced with a concrete (1, input_dim) dummy, no dynamic axes. (1, D) rather than rank-1 because jax2onnx lowers nn.Dense to Gemm, whose ONNX spec requires rank-2 (the sbi/bayesflow exporters trace rank-1 because torch lowers Linear to rank-agnostic MatMul+Add — different tracer, different constraint). Verified end-to-end against HSSM's make_jax_func + jax.vmap consumption with exact parity
  • Exports the eval-mode head: for logprob (LAN) this equals the raw head; for logits (CPN/OPN) it applies logsigmoid, matching every torch export path. Exporting the raw training head would silently corrupt every downstream logp by +log(1+exp(-logit)) — this was caught in review and is now explicitly regression-tested
  • torchtrain + transform-onnx untouched — they remain the verified fallback path

Tests (the parity test is the load-bearing one)

  • Contract: all input dims concrete, shape (1, D), op profile ⊆ {Gemm + elementwise} (guards against jax2onnx lowering changes under the 0.x pin)
  • Parity: ORT output vs live jax eval forward, 1000 draws, atol=1e-4; file-based transform ≡ in-trainer export
  • Logits head: CPN export == -log1p(exp(-raw)) and provably ≠ the raw head
  • Flag: --no-export-onnx produces no .onnx

Commands run

uv run pytest tests/   # 206 passed, 8 skipped, 1 xfailed
uv run ruff check src/lanfactory && uv run ruff format --check .

🤖 Generated with Claude Code

…ifacts

jaxtrain was a dead end for the ecosystem: it saved flax parameter bytes that
nothing downstream could read — transform-onnx only handles torch state dicts
and HSSM consumes ONNX exclusively. This closes the single missing edge.

- New lanfactory/onnx/jax_export.py: export_forward_to_onnx (shared core) +
  transform_jax_to_onnx (file-based, for retroactive conversion of existing
  .jax artifacts), exposed as the `transform-jax-onnx` CLI
- The jax trainer exports ONNX alongside its other artifacts (mirroring the
  torch trainer's _save_onnx) and logs it to MLflow; `--no-export-onnx` on
  jaxtrain opts out
- Dependency: jax2onnx==0.15.* (exact-pinned 0.x line), opset 17 to match the
  sbi/bayesflow exporters

Two contract decisions, both verified empirically:

- The graph is traced with a concrete (1, input_dim) dummy — every dim
  static, zero dynamic axes — matching the torch MLP exporter and the
  production networks on franklab/HSSM, NOT rank-1: jax2onnx lowers nn.Dense
  to Gemm, whose ONNX spec requires rank-2 inputs (a rank-1 trace is rejected
  by onnxruntime). Verified end-to-end that HSSM's make_jax_func loads the
  export and rank-1-per-trial + jax.vmap consumption matches exactly.
- The exported graph is the EVAL head: identity for logprob (LAN), logsigmoid
  for logits (CPN/OPN) — matching every torch export path (_save_onnx calls
  .eval(); transform-onnx exports under torch's EVAL default) and HSSM's
  element-wise log-likelihood consumption. Exporting the raw training head
  would silently corrupt every CPN/OPN logp by +log(1+exp(-logit)); a test
  asserts the logits export equals the logsigmoid head and is NOT the raw
  head.

Tests: contract (all dims concrete, (1,D) shape, op allowlist calibrated
against jax2onnx 0.15's actual lowering — relu emits Max, the logsigmoid
head emits Neg/Exp/Add/Log), parity (ORT vs eval-head jax forward over 1000
draws, atol 1e-4), file-based vs in-trainer export equivalence, logits-head
correctness, and the opt-out flag.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b9808482-ee71-4e2a-a137-7b2cad7ce50b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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