Add Axon.Loop.stream/4 and implement run/4 on top of it - #655
Open
seanmor5 wants to merge 1 commit into
Open
Conversation
Training loops are a map-reduce over a dataset, but `Axon.Loop.run/4`
collapses that into a single output, so any per-epoch logic has to go
through a `handle_event/4` handler.
`Axon.Loop.stream/4` returns a lazy stream which emits the accumulated
`%Axon.Loop.State{}` at the end of every completed epoch, making per-epoch
logic ordinary Enum/Stream code:
loop
|> Axon.Loop.stream(train_data)
|> Stream.map(&{&1.epoch, evaluate(&1.step_state.model_state)})
|> Enum.take(10)
The stream is infinite by default, so how long to train is decided by the
consumer rather than declared up front. `:epochs` is still accepted to
bound it.
Both functions now share a single engine, `loop_stream/5`, a
`Stream.resource/3` which drives the loop one epoch per `next_fun` call.
`run/4` is a reduce over that stream which keeps the final state and
applies the loop's `:output_transform`, so event ordering, batch function
compilation caching, halt semantics and metric zero-filling are unchanged.
Emitted states are the full loop state, not the `:output_transform` of it,
so metrics remain reachable. `:metrics` on an emitted state holds that
epoch's metrics, unlike the epoch => metrics map `run/4` returns.
polvalente
approved these changes
Aug 16, 2026
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.
Training loops are a map-reduce over a dataset, but
Axon.Loop.run/4collapses that into a single output. Any per-epoch logic — evaluating on a validation set, adjusting the dataset, logging something custom — has to be smuggled into ahandle_event(:epoch_completed, ...)handler that threads its results throughhandler_metadataor a closure.Axon.Loop.stream/4returns a lazy stream which emits the accumulated%Axon.Loop.State{}at the end of every completed epoch, so per-epoch logic becomes ordinaryEnum/Streamcode:The stream is infinite by default
Rather than declaring an epoch count up front, the consumer decides how long to train:
:epochsis still accepted if you want a bounded stream. The tradeoff is thatAxon.Loop.stream(loop, data) |> Enum.to_list()runs forever — inherent to an infinite stream, and called out in the docs.run/4is now a reduce over the streamBoth functions share one engine, a private
loop_stream/5built onStream.resource/3which drives the loop one epoch pernext_funcall and emits tagged elements —{:epoch, state}per completed epoch,{:done, state}once at the end with theepoch => metricsmap and:status.The epoch body is a direct transcription of the old
Enum.reduce_whileoverepoch_start..epoch_end//1, so event ordering, caching of the compiled batch function across epochs, halt semantics and metric zero-filling are unchanged.run/4keepsepochs: 1; changing that would break every existing caller, and "reduce to a single value" has no natural infinite default.Semantics worth reviewing
%State{}, notoutput_transform.(state). Otherwise a trainer's stream would yield only model state and drop the metrics, defeating the point.loop.output_transform.(state)is available if you want it.:metricson an emitted state is that epoch's metrics, not theepoch => metricsmaprun/4returns.:step_stateand:timesare cumulative.:halt_loopends the stream. This keeps "one element per completed epoch" crisp — the alternative would emit duplicate:epochvalues, since the existing:epoch_halted/:continuepath deliberately doesn't advancestate.epoch.%State{max_epoch: :infinity}is now possible. The field is write-only in the codebase — nothing reads it — so this is doc-only fallout.Streamsemantics.Tests
835 pass (was 826). New
describe "streaming"block covers per-epoch emission (epoch/iteration/step_state/times),epochs: 0, unbounded streams driven three ways off one stream value, laziness verified with anAgent-counting data stream, re-enumerability,:halt_loopterminating an unbounded stream, halted-epoch suppression, per-epoch metrics, andrun/4agreeing with the final stream state for a seeded trainer.🤖 Generated with Claude Code