Skip to content

13. External Dependency Contracts

This ledger records NNx integration points that depend on third-party packages, services, CLIs, or registries. It complements tests by naming the supported range, exact frozen resolution, consumed contract, and intentional test gates.

1. Purpose

Most integrations are optional extras. A mocked or skipped test does not prove that an external boundary still matches its upstream contract. Changes to an extra, CLI command, or published format must therefore update this page.

Exact package versions below come from uv.lock as checked on 2026-10-13. CLI versions are labeled as an audited local snapshot. Supported package ranges remain defined by pyproject.toml.

2. Contract Ledger

Integration Supported / frozen NNx contract relied on Verification
PyTorch training core torch>=2.4 / 2.13.0 (torchvision and torch-geometric are the vision / graph extras: see the domain-extras row) nn.Module, autograd, optimizer, torch.amp.GradScaler(device) + torch.autocast(device_type, dtype) for the FP16 (CUDA) and BF16 (CPU, CUDA) precision policy, torch.is_autocast_enabled(device_type), SDPA with float masks, torch.load(weights_only=True) for training-state sidecars Full frozen all-extras pytest matrix (current lane) plus the CI floor-deps lane on the declared minimum pair. See §2.1 for the tested matrix and what remains unverified.
Domain extras: vision, graph, plots torchvision>=0.19 / 0.28.0 (extra vision); torch_geometric>=2.4 / 2.8.0.post1 (extra graph); plotly>=5.18 / 6.9.0 (extra plots, also pulled by viz); domains = all three import nnx imports none of them; features import them on use through nnx._optional.require. It turns only the package's own absence into an ImportError naming the extra; a broken dependency of an installed package keeps its own error. Flat names (NNDataset, NNGraphDataset, the graph nets) resolve lazily to the same classes. nnx.__all__ lists them only when their extra is installed tests/test_core_install.py, CI installed-profiles and release artifact-profiles; see Domain extras verification below the table.
Explainability and model summary torchinfo>=1.8.0 / 1.8.0; captum>=0.7.0 / 0.9.0 (extra viz); ipython>=8.0 / 8.39.0 on CPython 3.10, 9.15.0 later (extra notebook) torchinfo.summary returning a ModelStatistics; Captum attribution classes (IntegratedGradients, Saliency, GradientShap, ...) with attribute(inputs, target=...); IPython.display.clear_output for live plots in notebooks tests/test_viz_summary.py, tests/test_viz_attribute.py and tests/test_callbacks.py (full frozen all-extras matrix).
ONNX export onnx>=1.15 / 1.22.0; onnxscript>=0.1 / 0.7.1 Legacy torch.onnx.export; optional dynamo=True only when supported tests/test_to_onnx_inputs.py, tests/test_onnx_dynamo.py, and tests/test_viz_netron.py; known exporter dispatch skew uses the documented guard in tests/conftest.py. The guard goes once the frozen torch / onnxscript pair dispatches every op the dynamo tests emit (no such skip in the all-extras lane). Structural (checker) evidence only, except the profiles in the next row.
ONNX Runtime execution (Export conformance) onnxruntime>=1.18 / 1.30.0 (1.23.2 on CPython 3.10, <1.24; 1.18 is the first to load IR-10 models), extra onnx-runtime InferenceSession(path, providers=["CPUExecutionProvider"]) and run on FP32 inputs; invalid input dimensions raise tests/test_export_conformance.py and the CI export-conformance job (not a required check): FeedFwdNN 4-8-2 under both exporters. Every raw logit at rtol=1e-4, atol=1e-5 for batches 1/2/3/7 of a batch-2 dynamic export. A wrong width refused; artifact SHA-256s verified before loading. The job cannot pass by skipping and keeps the JSON plus ONNX / external-data files. Executed parity is claimed for these two profiles only; quantized, Netron and GGUF / Ollama targets stay structural or container-only.
Netron viewer netron>=7.0 / 9.1.8 netron.start(path) only when launch=True; ONNX export remains independent tests/test_viz_netron.py covers launch dispatch and missing-package errors.
Hugging Face Hub huggingface-hub>=1.4.0 / 1.24.0; safetensors>=0.7.0 / 0.8.0 PyTorchModelHubMixin and safetensors checkpoint APIs tests/test_hub_mixin.py and tests/test_checkpoint_safetensors.py; authenticated pushes are intentionally credential-gated.
Experimental GGUF / Ollama bundle gguf>=0.19.0 / 0.19.0; audited source snapshots: Ollama tag v0.32.2, llama.cpp build 8660 GGUF container metadata and NNx tensor mapping under nnx_transformer; the tagged Ollama source's typed Modelfile parameters and bundle layout tests/test_interop_gguf_writer.py parses writer output and verifies bundle structure, rendering, all eleven documented parameter types, templates, and boundary validation. Stock llama.cpp, Ollama, and LM Studio execution remains explicitly unsupported because those runtimes do not implement nnx_transformer.
Quantization torchao>=0.17 / 0.17.0 PTQ INT8 and QAT 8da4w quantizer APIs tests/test_quantize_ptq.py and tests/test_quantize_qat.py; CUDA-only 2:4 behavior is hardware-gated.
Embeddings / FAISS faiss-cpu>=1.7 / 1.14.3; sentence-transformers>=2.7 / 5.6.0 FAISS index/search and SentenceTransformer-like forward(list[str]) -> Tensor[B, D] Embedding contrastive and FAISS export tests; downstream RAG adapters are out of scope.
LM data / tokenization tokenizers>=0.20 / 0.22.2; datasets>=2.20 / 5.0.1 BPE train/encode/decode and optional remote dataset loading Tokenizer and generative-model tests; network-backed dataset downloads are not required in core CI.
Jev models (TypeSafe API) typesafe-sdk>=0.7,<0.8 / 0.7.2 (pulls httpx2, tenacity), extra jev TypeSafeClient / AsyncTypeSafeClient(api_key=, base_url=, timeout=, retry=, transport=). system_one(state, questions, model=) with noul / choice / score question dictionaries. SystemOneResponse (model, usage, answers, request_id). The SDK RetryPolicy as the only retry owner, and its TypeSafeAPITimeoutError / TypeSafeAuthenticationError / TypeSafePermissionDeniedError / TypeSafeRateLimitError / TypeSafeAPIResponseValidationError hierarchy tests/test_decision_jev.py: a recording fake of system_one returning real SDK responses. It covers translation, alignment, metadata, confidence, typed failures with one attempt, client ownership and closing, failures through a decision job and the benchmark. tests/test_imports.py: no SDK imported by import nnx; the missing extra named. examples/decision_jev.py: real clients over an offline mock transport, sync and async. Live service behaviour is credential-gated: scripts/smoke_jev_live.py pins jev-1.13.0 and is run by hand, never by CI.
Budgeted search (Optuna) optuna>=4.0,<6 / 5.0.0 (pulls sqlalchemy, alembic), extra optuna optuna.create_study(study_name, storage, sampler, pruner, direction, load_if_exists=True), Study.optimize(objective, n_trials=1, gc_after_trial=False) run one trial at a time, Trial.suggest_float / suggest_int / suggest_categorical, Trial.report(value, step) + should_prune(), TrialPruned, study and trial user attributes, TrialState. NNx is the study's only writer, sequentially. It stores its search identity (space digest, monitor, direction, plan) on the study and refuses a mismatch. catch holds only NNx's failed-trial error. Only a failed fit, a missing or non-finite monitor value, or a run with no validated epoch becomes a failed trial. Any other exception stops the search. Sampler and pruner behaviour, storage engines and their concurrency are Optuna's tests/test_search.py: validation before any trial, fresh trials, every trial state counting against the budget, the update cap and an injected-clock deadline. Also pruning steps, non-finite values, selection, identity, cancellation. examples/search_offline.py: SQLite storage, reopened under budget. tests/test_imports.py checks import nnx loads no Optuna. Wall-time deadlines are non-preemptive.
Experiment logging tensorboard>=2.15 / 2.21.0; wandb>=0.16 / 0.28.1 Writer/run lifecycle and finish semantics Callback tests cover lifecycle and TensorBoard event output; real W&B service calls remain credential/network-gated.
Registered optimizer factories (nnx.optimizers) User-supplied callables registered in-process; no package, entry point or import-by-name factory(param_groups, config) -> torch.optim.Optimizer called once per optimizer with resolved groups (params, lr, weight_decay) and a read-only JSON-like config; the result must own exactly those parameters. run.yaml records only OptimizerFactorySpec(id, version, config); resume requires the same identity and parameter topology. tests/test_optimizer_factories.py (registry, one-call contract, ownership checks, failure before the run with no imports, resume validation, offline reload) and examples/optimizer_factories.py. Factory behaviour itself is the registrant's contract: NNx validates the returned optimizer's parameter set, not its update rule.
Maintenance tooling uv==0.13.0; pip-audit==2.10.1; Pyright 1.1.411; Ruff 0.16.10 Frozen resolution, exact-graph security audit, type and style gates Automation installs the same uv version declared in requirements-tools.txt; CI uses uv sync --frozen --all-extras; security exports that lock before auditing; Pyright warnings are gating.
Package publishing setuptools==84.0.0; uv==0.13.0; twine==7.0.0; PyPI OIDC Release version/tag agreement, reproducible artifact bytes, exact registry hashes, trusted publish, immutable GitHub release The top-level dispatch-only release workflow builds once for publication. It publishes to PyPI, installs from PyPI and checks the local/PyPI filename and SHA-256 sets. It then attaches the same artifacts to the GitHub release and verifies the API digests. It publishes the draft and then verifies its immutable attestation, which exists only once the release is published.

Domain extras verification. tests/test_core_install.py uses a core-only interpreter. It covers train / predict / reload, no domain module loaded, and each domain request naming its extra. It also covers lr_finder refusing before touching data / model / RNG and the NNRun repr without the chart. The test checks that the cause is kept for a broken dependency.

CI installed-profiles and release artifact-profiles install the built wheel per profile (core, each extra, domains) over the floor and current torch pairs. They run scripts/smoke_core_install.py, recording the installed closure and three cold-import timings (no threshold).

2.1. PyTorch support matrix

The floor in pyproject.toml is the oldest torch / torchvision pair on which the full core test suite passes. It is not merely the oldest release that imports. Every row states how it was exercised; a row without hardware evidence says so.

torch / torchvision Python Environment Evidence Status
2.13.0 / 0.28.0 (frozen uv.lock) 3.10 – 3.14 GitHub-hosted Ubuntu, CPU, all extras (uv sync --frozen --all-extras) CI lint-and-test matrix: the whole suite including extras, ruff, pyright, docs build. Tested — current.
2.4.1 / 0.19.1 (declared floor) 3.10 CPU, core dependencies plus the domains extra (no other extras). CI floor-deps lane on Ubuntu (CPU wheels from download.pytorch.org/whl/cpu), and a local macOS arm64 run on 2026-09-22 Whole core suite: 1714 passed, 70 skipped (extra-gated importorskip), docs-projection modules and the Cora download test excluded. Tested — minimum.
2.5.1 / 0.20.1 3.10 Local macOS arm64 CPU run on 2026-09-22, the then-core dependencies (graph, vision and plotting stacks included — today's core plus domains) Whole core suite, same exclusions as the floor row: 1714 passed, 70 skipped. Tested — intermediate (not a CI lane).
2.3.1 / 0.18.1 (with numpy<2) 3.10 Local macOS arm64 CPU run on 2026-09-22 23 failures in library code. torch.is_autocast_enabled(device_type) raises TypeError in the TransformerNN forward (13 tests). SDPA rejects a float attn_mask whose dtype differs from a half / bf16 / double query (9). torch.load(weights_only=True) cannot unpickle the training-state sidecar (1). torch.amp.GradScaler("cuda") itself exists from 2.3. Unsupported — excluded by metadata.
< 2.3 — not run torch.amp.GradScaler does not exist; the CUDA AMP factory would raise AttributeError. Unsupported — excluded by metadata.
CPU BF16 (PrecisionPolicy("bf16") on a CPU device) on the CI rows 3.10 – 3.14 the CI lint-and-test and floor-deps lanes (CPU) tests/test_precision_execution.py: seeded classification and KD fixtures with accumulation stay within nnx.precision.REFERENCE_TOLERANCES["bf16"] of their FP32 reference. Also checked: bf16 autocast without a scaler, the backward outside autocast, float32 parameters, and the finite-gradient check before clipping. Tested — CPU (precision_support() reports it "verified").
CUDA FP16 / BF16 (PrecisionPolicy("fp16" / "bf16"), or mixed_precision=True, on a CUDA device) on any row — no CUDA hardware in the local or CI environments used for this matrix Stubs and CPU simulation only; the CUDA evidence tests have not run yet. See CUDA precision evidence below the table. Unverified — control-flow coverage, not hardware execution.

CUDA precision evidence. Resolution is covered by stubs (tests/test_precision_policy.py, and tests/test_train_integration.py::test_grad_scaler_prefers_modern_factory / ::test_grad_scaler_disabled_on_cpu_or_without_mixed_precision). The FP16 update order (unscale → normalize → clip → step through the scaler) and the scaler's checkpoint round trip are simulated on CPU. test_cuda_cells_are_verified_only_on_cuda_hardware runs the reference fixtures only on CUDA hardware. Otherwise it asserts precision_support() reports the cells "unverified". examples/02_resume_training.py::amp_resume_compatibility runs its enabled branch only when torch.cuda.is_available().

tests/test_cuda_evidence.py (composition rows TC-06, TC-10, TC-11) trains FP16 / BF16 and a two-GPU NCCL run on CUDA. It is evidence only from a cuda evidence-profile report (CONTRIBUTING §5.1, "GPU evidence"), and none has run yet.

Optional extras carry their own constraints and are not part of the floor lane. torchao ([quantize]) is validated only against the frozen torch above. pyg-lib / torch-sparse (neighbor-sampler iteration) are never installed. CUDA-only quantization behaviour is hardware-gated.

NNx uses a release-please-managed static package version. Wheels and sdists from untagged commits are local test artifacts only and must not be distributed. Post-release source changes retain the preceding release number until the next release PR.

Release Please dispatches the top-level release.yml workflow, which is the sole distribution path; direct tag pushes do not publish. Keeping trusted publishing in that workflow preserves the PyPI OIDC identity. Meanwhile the workflow revalidates the tag SHA, checks tag/version agreement, and verifies exact artifacts on PyPI and GitHub.

Repository release immutability and protected v* tags apply to releases created after the 2026-07-22 hardening. The historical v0.2.1 GitHub release predates that control, remains mutable, and has no attached distribution attestations. It is retained as published history rather than destructively recreated; PyPI remains the artifact source for that version.

3. Review Rules

  1. Update this ledger with any dependency range, optional extra, external CLI, or published configuration change.
  2. Prefer tests against the frozen public API. Use a mock only for credentials, hardware, daemons, or network boundaries, and record the gate here.
  3. Document tolerated upstream skew together with the condition for removing its compatibility guard or skip.