Reinforcement Learning

Millions of environment steps. None of them lost.

From parallel simulation rollouts to policy updates on multi-node GPUs, Union runs RL training loops with spot-safe checkpointing, reproducibility by design, and cloud-native scaling.

Try the devbox

A free, local sandbox to
explore the Union.ai platform.

Chat with an engineer

Trusted by leading AI teams

Built on Flyte

Open source at the core.

Union is built on Flyte, the open-source AI runtime we create and maintain under the Linux Foundation AI & Data.

4000+

companies using Flyte today

17M+

Flyte SDK downloads

Fan-Out & Scale

Fan out on durable asyncio.

A policy learns as fast as the simulator feeds it. Every iteration wants hundreds of environments stepping at once, and the sampling is CPU work while the update is GPU work. Fanning out over rollouts is plain async, and the cluster scales back to zero between experiments.

  • Concurrency you can cap. `flyte.map.aio(fn, seeds, concurrency=256)` bounds how many environments run at once, for a fan-out wider than the cluster.
  • CPU sims, GPU learner, one workflow. Each task declares what it needs, so hundreds of cheap simulator containers feed a single expensive GPU step instead of the pipeline sizing everything for its largest node.
  • Built for spot. `interruptible=True` puts rollout workers on spot nodes and cuts compute costs by more than 90%. Losing an episode batch is expected here, not an incident.
Function-Level Checkpointing

Recover. Fork. Replay.

Training curves are the one thing you cannot rerun cheaply. A policy that took four days and ten thousand iterations to get interesting is gone if the run dies at iteration nine thousand. Union records what finished as the run happens, outside the node doing the work, and that record is what you resume from.

  • Recover what failed. Point `recover` at a prior run. Finished iterations are reused and only what failed or changed runs again, even without caching enabled.
  • Checkpoints that survive the node. `flyte.Checkpoint` saves policy and optimizer state each iteration, so a preemption resumes at the iteration it stopped rather than at a random initialization.
  • Fork a reward function, keep the policy. `flyte.rerun` branches a prior run at the iteration you pick, so a new reward shaping term starts from a trained policy instead of from scratch.
Infrastructure as Context

Except blocks can change the hardware.

Replay buffers grow until something gives. A buffer sized for a toy control task fits in 8Gi, and the same code on pixel observations with a longer horizon does not. Union hands you a typed error instead of a stack trace, so the step that blows memory comes back as an exception you can catch and re-run on a bigger box.

  • Typed infrastructure errors. `OOMError`, `TaskInterruptedError`, `TaskTimeoutError`, `ImagePullBackOffError`. Failures you can branch on.
  • Resources changed at runtime. `.override(resources=...)` re-runs the same update with more memory, or moves it from an A100 to an H100.
  • A hung simulator is a typed error too. `TaskTimeoutError` catches the environment that stopped stepping, so one bad seed does not stall the iteration behind it.
  • The handler does not have to be human. Provisioning is an ordinary Python call, so the same API you write a retry policy against is one an agent can call while a run is in flight.
Durable Artifacts

Outputs that outlive the run.

A policy is only useful once something else can load it. Artifacts are typed, versioned values that persist past the run that made them, so an evaluation suite or a sim-to-real deployment picks up a checkpoint without anyone moving weights between buckets, and a promoted policy can trigger the next benchmark by itself.

  • Passed between workflows and apps. A training run's policy is an eval workflow's typed input, without re-running the producer.
  • Versioned, not overwritten. A new version is a new artifact, so a regression names the exact policy that scored better last week.
  • Events, not polling. `flyte.OnArtifact` fires a benchmark whenever a new policy is promoted, with no cron hacks in between.
Run History & Versioning

Reproduce a run from months ago.

RL results are famously hard to reproduce, and the seed is the smallest part of why. Every run keeps what it takes to reproduce a result: the code that executed, the infrastructure it ran on, and the configuration applied to both. That is what makes a training loop shareable rather than personal, and it is what a paper or a postmortem needs.

  • Inputs and outputs per run. Open a run from months ago and see the exact reward function, seeds, and environment version behind a policy.
  • The code that ran, not the code today. Each run resolves to its own code bundle and container image, down to the simulator build.
  • Watch it learn, inside the run. `report.log()` renders reward curves and episode replays in the execution itself, so an experiment documents its own behavior instead of pointing at a dashboard that has since rolled over.
Pure Python

Training loops in pure Python.

Rollouts, updates, checkpoints, and replays, no YAML, no DSLs. Write it in Python, run it across hundreds of workers on Union.

import asyncioimport flytefrom flyte import reportfrom flyte.io import File # Hundreds of cheap CPU containers step the simulatorsim_env = flyte.TaskEnvironment(    name="sim",    image=flyte.Image.from_debian_base().with_pip_packages("mujoco", "gymnasium"),    resources=flyte.Resources(cpu=4, memory="8Gi"),) # One expensive GPU container does the policy updatelearn_env = flyte.TaskEnvironment(    name="learn",    resources=flyte.Resources(gpu="A100:4", memory="64Gi"),    depends_on=[sim_env],)  @sim_env.task(cache="auto", retries=3, interruptible=True, timeout=600)async def rollout(policy: File, seed: int) -> File:    """Collect one episode batch. Cached per policy and seed."""    env = gymnasium.make("Humanoid-v5", seed=seed)    return await File.from_local(collect(env, policy, n_episodes=200))  @learn_env.task(retries=5, interruptible=True)async def train(iterations: int) -> File:    """A preemption resumes at the last iteration, not iteration zero."""    ckpt = flyte.Checkpoint.current()    policy, start = await ckpt.load() or (PPO(), 0)     for i in range(start, iterations):        # 256 environments step at once, bounded by the pool        batches = [b async for b in flyte.map.aio(            rollout, seeds(i), concurrency=256, policy=policy)]        policy = policy.learn(batches)        await ckpt.save((policy, i + 1))        # Reward curve and a replay, rendered inside this run        report.log(reward_curve(policy), episode_replay(policy))     return await File.from_local(policy.save())

CPU sims, GPU learner

Two environments in one workflow: hundreds of four-core simulator containers feeding a single four-A100 update step. depends_on ships both images together.

Bounded rollout fan-out

flyte.map steps 256 environments per iteration with concurrency capped. A hung seed hits timeout and retries in place instead of stalling the iteration.

Checkpoint every iteration

Policy and iteration index save together, so a preempted spot node resumes at iteration 9,000 rather than starting over. report.log leaves the reward curve in the run itself.

Start today and scale with confidence.

Talk to our team about how Union runs simulation, training loops, and evaluation for teams building agents that learn from experience.

Try the devbox

A free, local sandbox to
explore the Union.ai platform.

Chat with an engineer