Skip to content

feat: add a PyTorch backend alongside Keras - #2

Merged
vyncint merged 2 commits into
mainfrom
feat/pytorch-support
Jul 27, 2026
Merged

feat: add a PyTorch backend alongside Keras#2
vyncint merged 2 commits into
mainfrom
feat/pytorch-support

Conversation

@vyncint

@vyncint vyncint commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Adds a PyTorch backend. The Keras API is untouched and no existing checkpoint needs converting.

The shape of it

PyTorch has no fit, so TorchTrainer takes the training step from you and calls it once per round:

from infinite_training import TorchTrainer, Target

def step() -> dict[str, float]:
    model.train()
    optimizer.zero_grad()
    loss = loss_fn(model(x), y)
    loss.backward()
    optimizer.step()
    return {"loss": loss.item()}

trainer = TorchTrainer(model=model, target=Target("loss", True, 1e-4), timeout=60)
trainer.train(step)
predictions, best_loss = trainer.predict_best(x)

Everything around the step — the Target, the round loop, the timeout, the Ctrl+C handling, the checkpointing and the resume behaviour — is shared with the Keras trainer. One round is one call to your step function, so you choose the granularity at which the target and timeout are checked.

There is no compile(): the shadow copy holding the best weights is deep-copied in the constructor, so inference works straight away.

Three decisions worth reviewing

Backends are now imported lazily. import infinite_training no longer imports TensorFlow at all, and referencing TorchTrainer never imports it either. Having only one framework installed is enough. Asking for a trainer whose framework is missing raises an ImportError naming the extra:

TorchTrainer requires torch, which is not installed.
Install it with: pip install 'infinite-training[torch]'

TensorFlow stays a hard dependency for 2.x. Moving it to an extra now would break every existing pip install infinite-training, which is not a minor-version change. So [tensorflow] and [torch] extras are both declared, TensorFlow also stays in dependencies, and the CHANGELOG and README say it moves to the extra in 3.0.0. A torch-only user can already opt out with pip install --no-deps infinite-training plus numpy torch. If you would rather take the break now, this is the piece to change.

Checkpoints are NumPy state dicts, not torch.save archives. This keeps the storage format consistent with the Keras side, lets a checkpoint written on a GPU resume on a CPU with no device map, and keeps the file readable without PyTorch installed. There is a test that the int64 buffer in BatchNorm round-trips with its dtype intact.

Shared internals

The loop, the target and timeout bookkeeping, the value history and the checkpoint paths moved into an internal _base module that both backends use. InfiniteTrainer lost about 70 lines and gained four small hooks; no public name changed, and the existing 55 tests pass without modification.

Testing

  • 93 tests pass, up from 55: 31 for the PyTorch backend, 7 for backend isolation.
  • Both frameworks installed: 93 passed.
  • Only PyTorch installed: 55 passed, 1 skipped — the Keras modules are skipped via collect_ignore.
  • Only TensorFlow installed: 61 passed, 1 skipped.
  • ruff check and ruff format --check clean; uv build plus twine check pass on both artifacts.

The isolation runs are the ones that matter for the lazy-import claim, so there is a new CI job that installs each framework on its own, asserts the other is genuinely absent, and runs the suite.

Coverage for the PyTorch path includes the stop conditions, best-weight tracking for both minimised and maximised targets, the metrics contract (missing key, non-mapping return, sequence values, tensors, NumPy scalars, empty sequences), KeyboardInterrupt still checkpointing, resume from disk, parent-directory creation, and that inference runs in eval mode under no_grad and restores the previous training mode afterwards.

One thing I got wrong while writing this

My first version of the multi-round test targeted a loss below 0.24 on the XOR-shaped fixture with no timeout. A single Linear(2, 1) cannot separate XOR, so its MSE floor is exactly 0.25 — the test hung rather than failed. It now targets 0.26 with a 60 second backstop, and the reason is in a comment so nobody re-tightens it.

Version

Bumped to 2.2.0. Additive feature, no breaking change.

PyTorch has no fit(), so TorchTrainer takes a step function that runs one round
and returns its metrics. Everything around that - the Target, the round loop,
the timeout, the Ctrl+C handling, the checkpointing and the resume behaviour -
is shared with the Keras trainer, which is unchanged.

The shared parts moved into an internal _base module. No public name changed and
the existing 55 tests pass untouched.

Backends are now imported lazily, so having only one of the two frameworks
installed is enough: importing TorchTrainer never loads TensorFlow, and asking
for a trainer whose framework is missing raises an ImportError naming the extra
to install. TensorFlow stays a hard requirement for the 2.x line so existing
installs keep working; it moves to the `tensorflow` extra in 3.0.0.

Adds 39 tests (94 total), a CI job that installs each framework on its own, and
a PyTorch MNIST example.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 27, 2026 23:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

The test job segfaulted (exit 139) as soon as a torch test ran after the Keras
ones. The traceback is entirely inside torch._dynamo, and the loaded extension
modules include cuda.bindings.*: the default PyTorch wheel bundles CUDA
libraries that clash with the ones TensorFlow loads once both are imported into
the same process.

The runners have no GPU, so the CPU wheel is the correct choice regardless, and
it is a much smaller download. The backend-isolation jobs already passed - they
only ever import one framework - but they now use the CPU build too, for
consistency and speed.

Also documents the hazard in the README, since a user installing both
frameworks can hit it independently of this package.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vyncint
vyncint merged commit 500c2d8 into main Jul 27, 2026
7 checks passed
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.

2 participants