Skip to content
Skip to lesson
Back to Topics
LearnProject Deep DivesDeep Dive - SkyRL
⚡HardFine-Tuning & Training

Deep Dive - SkyRL

A SQL repair is still in flight when two replicas load new weights and a third keeps the old ones. SkyRL has to record which policy produced each action.

44 min read
Learning path
Step 184 of 199 in the full curriculum

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

A step-12 SQL-repair run can hit this boundary: a trajectory is still waiting on its database when training finishes an update. Two serving replicas load the new weights. A third answers the next turn with the old ones. The query can still earn a good reward, yet the run no longer knows which model version produced each action.

Follow one trajectory, query-42_0, from prompt through generator, environment, trainer, and weight sync. Its token IDs, loss masks, reward, and identity must survive every handoff. Version tracking needs extra care: a scheduled trainer step is recorded for each async group, but that number isn't a per-token record of the weights used during an in-flight update.

The implementation walkthrough is pinned to official SkyRL commit 955c3e23bf8939b28a9d6c424308a762dcb8a2e7. Its selected source contracts and living documentation were rechecked September 23, 2026. It uses the unified skyrl/ package; skyrl-gym and skyrl-agent remain separate directories. Older skyrl-train and skyrl-tx recipes use different paths. Match a recipe to its commit before copying commands.[1]

Replaceable interfaces, not one trainer

Suppose query-42_0 becomes a terminal task tomorrow. Which code should change? The environment may change from SQL execution to shell commands, and the generator may change how it parses turns. Advantage calculation should keep receiving the same trajectory contract. That is the boundary to look for before opening a trainer file.

SkyRL names four working seams: trajectory generator, environment, inference engine, and trainer. A controller role also owns placement, initialization, and control flow, although current training control still lives in trainer.py while extraction of a standalone controller remains work in progress. Explicit inputs and outputs keep the trainer from knowing every task implementation.[2]

This makes an extension local. A Text-to-SQL task adds an environment or generator; it doesn't edit advantage calculation. A local vLLM worker can be replaced by a remote OpenAI-compatible endpoint without moving reward processing. One-step pipelining can become fully asynchronous while the trajectory fields stay recognizable. The schedule and freshness policy change, but the handoff still answers the same ledger questions.

The native trainer includes , , and several policy-gradient variants. Its documented fully async backends are and . The Tinker path also includes ; backend presence doesn't imply every trainer supports it. HTTP inference uses vLLM API servers plus a router.[1][3]

💡 Key insight: If a seam preserves prompt, token, reward, mask, and version semantics, the implementation behind it can change without changing the learning loop.

Project identity and lineage

The seams are code boundaries, not an org chart. SkyRL is developed at the Berkeley Sky Computing Lab in collaboration with Anyscale. Its repository acknowledges compute support from Databricks, NVIDIA, Lambda Labs, AMD, AWS, Modal, and Daytona. Those acknowledgments explain context. They don't assign every subsystem to every organization.[1]

FieldCurrent project fact
OriginNovaSky AI, a Berkeley Sky Computing Lab initiative, built SkyRL with academic and industry collaborators.[1]
Founding contributorsSkyRL-v0 and SkyRL-Agent name Shiyi Cao, Dacheng Li, Sumanth Hegde, Richard Liaw, Philipp Moritz, Matei Zaharia, Joseph Gonzalez, Ion Stoica, and collaborators.[1][4]
Contributor modelThe public development guide documents contributions, tests, environments, generators, algorithms, and trainer changes. Those reviewed pages don't establish a separate TSC or committer charter.[5]
Source licenseThe repository root uses Apache-2.0.[6] Package metadata, copied components, and third-party dependencies can carry additional terms, so audit the package you redistribute.
Commercial boundaryAnyscale and listed compute providers are collaborators or supporters, not owners of every SkyRL component.[1]
Asset boundaryTraining recipes can download external checkpoints and datasets whose licenses aren't replaced by SkyRL's source license.

The lineage is visible in the repository layout and release notes:

Lineage pieceWhat it contributedWhy it still matters
SkyRL-v0Long-horizon, multi-turn tool-use RL for real environmentsEstablishes the agent-training problem and trajectory shape
SkyRL-v0.1 / skyrl-trainModular trainer, placement options, PPO and Makes algorithms and execution plans replaceable
SkyRL-Gym math, coding, search, and environmentsGives task authors a small environment contract
SkyRL txTinker-compatible backend for local hardwareSeparates a user-facing training API from engine placement
SkyRL-AgentAgent tasks, tools, async dispatch, and backend adaptersMoves long-horizon loops into a reusable agent layer
Harbor integrationTerminal-use environments wired into the trainerShows the environment seam can absorb an external harness
skyrl/Unified package for training, inference, and Tinker pathsReduces split-brain documentation during reorganization

The project also credits ideas and code from veRL, OpenRLHF, Search-R1, OpenReasonerZero, and NeMo-RL. That lineage maps the ecosystem. It doesn't make SkyRL a drop-in replacement for any one project, and it doesn't say what the trainer still needs after two SQL turns.[1]

Generator, environment, trainer

Return to query-42_0. Before naming classes, predict what the trainer needs after two SQL turns: the sampled response, the database observation, the reward, and a way to distinguish trainable tokens from context. If that record is complete, the optimizer can stay unaware of whether the environment is SQL, a terminal, or a browser.

A controller role sets up execution. A Generator coordinates inference and environments to produce trajectories. A Trainer turns those trajectories into . Current control flow remains in trainer.py, while InferenceEngine and Environment stay replaceable boundaries inside the Generator.

The loop isn't "model in, loss out." A multi-turn trajectory can include token IDs, tool calls, observations, rewards, per-token masks, and a stop reason. Those fields carry the decision from environment execution into optimization. Without them, a trainer can apply loss to an observation, credit a failed tool call as success, or join a retry to the wrong episode.

Vocabulary at the seams

The table is a decoder ring for the code. Read Rollout as the artifact being handed forward, and read the final column as the bug that appears when one boundary silently changes its meaning.

TermMeaning hereFailure if misunderstood
RolloutOne model interaction or multi-turn trajectory collected from an environmentTreating a partial turn as a complete training example
GeneratorComponent that turns prompts and environment configuration into trajectoriesPutting task-specific loops inside the trainer
Environment that consumes actions and emits observations, reward, and terminationAccepting unverified model-written scores
Inference engineRuntime that samples tokens and often returns log probabilitiesAssuming the training model and sampler always share weights
Training inputTokenized prompt, response, masks, rewards, advantages, and metadata used by optimizationApplying loss to tool or padding tokens by accident
StalenessDifference between scheduling step and consumption stepOptimizing on data generated by a policy far in the past
Weight syncTransfer of updated policy parameters to inference workersSampling with an old policy while labeling it on-policy

GeneratorInput includes prompts, environment classes, optional environment extras, sampling parameters, trajectory IDs, and batch metadata. GeneratorOutput returns batched prompt and response token IDs, rewards, loss masks, stop reasons, rollout metrics, optional log probabilities, trajectory IDs, and timing splits. It also includes step-wise completion flags and vision . TypedDict describes the interface for type checkers; it doesn't validate a custom generator's arrays at runtime.[7]

Here Optional[...] means a field's value may be None; it doesn't make that key optional. The pinned TypedDict uses required keys. A real extension must populate its declared fields, using None where appropriate, as well as preserve array alignment.[7]

The architecture in one request

The same handoff must work for a one-turn math answer and a long tool-use task. A generator may call an environment many times before returning one output, but the trainer still receives one normalized object. That normalization is what lets us reuse the rest of the route for query-42_0.

Diagram showing Prompt batch, Generator, actions / observations, and Environment.
Prompt batch, Generator, actions / observations, and Environment.

The two feedback paths have different jobs: inference samples actions, while the environment executes them and returns observations. The trainer receives completed records rather than raw tool traffic. Zero- rewards, broken tools, stale groups, and failed transfers need separate handling at those boundaries.

A concrete trajectory

Now follow the ledger instead of the class diagram. The prompt is: "List active customers with more than three unresolved incidents." The generator asks for a SQL action, sends it to a database environment, appends either an error or rows as an observation, and gives the model another turn to repair its query. The environment supplies evidence; the loss mask decides which tokens can move the policy.

Call this trajectory query-42_0. Should the database error receive policy loss? No: it's context, not a sampled action. To keep the alignment visible, use a toy vocabulary with two tokens for an attempted query, one observation token, and two tokens for its repair. These IDs don't come from a real tokenizer, and the labels aren't executable SQL. This reduced view shows one row of a batched output; other declared fields are omitted for legibility, so it isn't a complete typed return object:

FieldExampleWhy trainer needs it
prompt_token_ids[[101, 921, 3321]]Keep the prompt separate from the response
response_ids[[11, 12, 90, 21, 22]]Preserve actions and intermediate observations
rewards[[0, -0.2, 0, 0, 1.0]]Place each turn's reward at its final action token
loss_masks[[1, 1, 0, 1, 1]]Exclude the observation from the policy-loss objective
stop_reasons["stop"]Store one final reason per output row
trajectory_ids[TrajectoryID("query-42", 0)]Render as query-42_0 for log joins

The reward field accepts either one scalar per trajectory or one reward per response token. It isn't an arbitrary list of turn rewards: the default multi-turn generator stamps each turn reward onto the corresponding final action token. Here the total is 0.8; aggregation and advantage estimation decide how that return influences the sampled actions.[7][8]

Five toy response positions align token IDs, masks, and rewards. The environment observation at position 2 stays in context with mask zero. Turn rewards occur at action endpoints 1 and 4.
Arrays align to response positions, including the observation. Reward is stamped at each action turn's end. Mask 0 removes the observation's direct policy-loss term, but later actions still attend to it.

The IDs and masks aren't decoration. SkyRLGymGenerator enforces in its default multi-turn path: it appends sampled token IDs, appends tokenized observations with mask 0, then samples again. If you retokenize the whole chat from strings, token boundaries can change after the sampler has finished. Training then sees a different sequence from the one that earned the reward.[9]

A multi-turn batch can flatten several conversations. Step-wise training can return one turn at a time and mark whether an episode is complete. Keep those distinctions attached to each row, or a retry and its reward can be joined to the wrong turn.

The toy row below checks alignment and computes the masked mean negative log probability for sampled actions. GRPO training adds group-relative advantages and clipped policy-ratio terms; this alignment check leaves those terms out. Changing the observation's supplied score doesn't change this objective, while changing a sampled action's score does.

skyrl_response_mask.py
from math import isclose, isfinite

token_ids = [11, 12, 90, 21, 22]
masks = [1, 1, 0, 1, 1]
rewards = [0, -0.2, 0, 0, 1.0]
assert len(token_ids) == len(masks) == len(rewards)

def masked_nll(logprobs, mask):
    if len(logprobs) != len(mask) or any(m not in (0, 1) for m in mask):
        raise ValueError("unaligned or nonbinary mask")
    selected = [lp for lp, m in zip(logprobs, mask) if m]
    if not selected or not all(isfinite(lp) and lp <= 0 for lp in selected):
        raise ValueError("need finite action log probabilities and a nonempty mask")
    return -sum(selected) / len(selected)

logprobs = [-0.2, -0.4, 0.0, -0.6, -0.8]
baseline = masked_nll(logprobs, masks)
changed_observation = logprobs[:]
changed_observation[2] = float("nan")
assert masked_nll(changed_observation, masks) == baseline
changed_action = logprobs[:]
changed_action[4] = -1.2
assert isclose(masked_nll(changed_action, masks), 0.6)
for bad_mask in ([1], [0] * 5, [1, 1, 2, 1, 1]):
    try:
        masked_nll(logprobs, bad_mask)
    except ValueError:
        pass
    else:
        raise AssertionError("invalid mask accepted")
print(f"trainable_tokens={sum(masks)} total_reward={sum(rewards):.1f} masked_nll={baseline:.2f}")
Response-mask boundary
trainable_tokens=4 total_reward=0.8 masked_nll=0.50

Masking the observation's loss doesn't remove it from attention or prevent gradients from depending on its context. The check changes a supplied score, not the observation tokens fed to a real model.

Read the main code paths

With the artifact in hand, code reading has an order. Start at skyrl/train/trainer.py, then follow the generator contract and the environment adapter. The trainer initializes weight-sync state, prepares the sampler, optionally evaluates, and requests a batch. Each next operation consumes the previous operation's output, so a missing field is easier to locate at the handoff where it disappears.

The generator contract lives in skyrl/train/generators/base.py. Its abstract method is asynchronous even for a synchronous RL schedule. A browser, remote verifier, or tool process can be waiting while the generator yields control. A custom generator has one required method, generate(), and returns output in input order. That order is another part of the ledger: IDs and rewards must still line up when the batch comes back.

The environment path stays outside the trainer. SkyRLGymGenerator adapts , while examples also integrate verifiers, OpenEnv, Harbor, and Terminal-Bench. The agent layer adds another adapter for long-horizon loops. A new task therefore changes the edge that executes actions and computes reward, while the core still consumes the same output contract.

The trainer's order of operations

Read the regular loop as three handoffs.

First, prompts and sampling parameters enter the generator; trajectories come back with rewards, masks, IDs, and any timing metadata. Dynamic sampling and step-wise merging then decide which complete records reach the trainer.

Second, SkyRL converts those records into training input and computes current log probabilities, values when a critic is configured, advantages, and returns. Only then do policy and optional critic workers update.

Finally, checkpoints and evaluation produce evidence, and weight sync publishes the resulting policy to the next sampling window.

In the synchronous schedule, the batch's environment interactions finish before optimization, and weight sync completes before the next batch starts. Fully async deliberately relaxes that ordering: other groups can still be interacting with environments while this batch trains. Neither schedule replaces sandbox isolation, correct token alignment, or explicit version logging.

Synchronous RL: the reference schedule

Start with a schedule that separates sampling from updates. Generate a batch with policy version πk\pi_k, wait for every response and reward, update to πk+1\pi_{k+1}, then send that version to the sampler. The next batch starts only after every sampler crosses the sync boundary. For query-42_0, the whole SQL repair finishes before weights change. A partial or unverified transfer can still break this contract, even in synchronous mode.

Why keep this slower baseline? It gives policy-gradient math a clean reference. For a response yy sampled from prompt xx, the trainer uses an objective of the form:

L(θ)=−E(x,y)∼πk[A(x,y)log⁡πθ(y∣x)],L(\theta) = -\mathbb{E}_{(x,y) \sim \pi_k}\left[A(x,y) \log \pi_\theta(y \mid x)\right],

where A(x,y)A(x,y) is an advantage estimate. Minimizing this objective encourages higher probability for positive-advantage actions and lower probability for negative-advantage actions. Shared parameters couple updates across actions and samples, so an individual action's probability can move either way. clips the probability-ratio objective to limit incentives for large changes. The resulting policy can still move beyond the clip range because clipping does not constrain every final probability ratio.

GRPO replaces a learned value baseline with relative scores within a group of samples for the same prompt. Identical rewards give zero centered reward advantage. An enabled KL penalty or another auxiliary term can still contribute gradients, so zero reward variance doesn't universally mean zero optimizer update.

Some recipes also divide by the group standard deviation; the tiny example below uses mean centering only, matching the Dr. GRPO-style optional skip.

What synchronous training buys

Use this table as a baseline for the async choices that follow. Each benefit comes from waiting at one visible boundary; each cost is the idle time that boundary exposes.

PropertySynchronous behaviorPractical consequence
Policy freshnessBatch is sampled before one updateEasier on-policy reasoning
DebuggingClear start and end for each stepReplay logs by global step
Hardware useTrainer or sampler may waitIdle time on long tools
Failure isolationStep fails as one unitSimpler retry, slower recovery
Metric interpretationThroughput per completed batchEasier baseline for async comparisons

The cost appears when tools are slow or trajectories have uneven lengths. One browser task can hold an entire step open while other GPUs wait. Synchronous mode is still the right first implementation for a new environment: it limits moving parts and gives you a trustworthy reward, mask, and version baseline before overlap enters the picture.

One-step async and fully async

The baseline exposes the tension: waiting protects freshness but lets a straggler idle the cluster. Suppose three SQL repairs finish in four seconds and query-42_0 needs forty. One-step off-policy pipelining can generate the next batch while the current one trains, but a slow generation batch still stalls its own handoff. Fully asynchronous training goes further: generation workers keep producing groups in a bounded buffer, and after each trainer step the inference client pauses in-flight requests, syncs weights, and resumes those trajectories.[3]

That distinction predicts the debugging surface. One-step overlap adds a pipeline boundary. Fully async adds a freshness budget, queue-capacity control, pause/resume around weight sync, and metrics that tell you how old each group was when consumed. A throughput gain without those measurements leaves the opening incident hidden.

The fully async implementation lives in fully_async_trainer.py. It creates one asyncio.Task per generation worker, places completed groups in an asyncio.Queue, and uses an AsyncStalenessManager to limit producer capacity. Here a GRPO group is n_samples_per_prompt trajectories for one prompt. Control stays within an epoch: SkyRL doesn't run generation from epoch t+1t+1 while epoch tt is still training.[3]

Legal async counter trace with mini-batch B=1 and lag budget S=2. Long group L starts at step 1 and is consumed at step 4. Its lag is 3, while outstanding groups are 3,3,2,1 and never exceed the headroom of 3.
This legal counter trace has no wall-clock speedup claim. L stays running while three short groups train. At consumption step 4 its scheduling lag is 3, even though outstanding work never exceeds B(S+1)=3.

Staleness as a budget

max_staleness_steps limits the aggregate admission budget; individual trajectories can exceed that value. Let sis_i be the trainer's current global step when group ii is consumed and gig_i be the step when generation was scheduled. SkyRL records:

staleness⁡i=si−gi.\operatorname{staleness}_i = s_i - g_i.

If query-42_0 starts at step 1 and reaches the trainer at step 4, its staleness is 3. The admission controller isn't a rule that drops every group whose number exceeds SS. With mini-batch size BB and max_staleness_steps SS, at global step ss it keeps

accepted+running≤(S+s) B.\text{accepted} + \text{running} \le (S + s)\,B.

accepted is cumulative across training: it includes completed groups and the current queue. At the start of step ss, completed training has consumed (s−1)B(s-1)B groups. Subtracting that count leaves at most B(S+1)B(S+1) groups buffered or running. A separate worker-concurrency bound can reduce admission further. With S=0S=0, only the current mini-batch fits, so generation can't get ahead of its update.[10]

Scheduling lag also differs from exact policy provenance. A long trajectory can resume under new weights several times. Its recorded start step doesn't identify the behavior policy for every token; preserve rollout log probabilities and log sync boundaries when interpreting correction ratios.

The inequality is aggregate. With max_staleness_steps = 2, a long query-42_0 group can still arrive with staleness 3. Current behavior accepts it and logs a warning. Track the full lag distribution alongside submitted, running, accepted, and filtered counts. Frequent high-lag groups point to a producer outrunning the consumer or generation latency with a long tail.

That budget comes with configuration constraints. The docs limit fully async to generators that talk to /chat/completions. generator.batched must be false, dynamic sampling must be disabled, train_batch_size must equal policy_mini_batch_size, and colocation of all training and inference workers isn't supported. The pinned trainer checks dynamic sampling against Python None; use YAML null in the sketch below.[10]

Instead of synchronous dynamic_sampling, fully async offers sample_full_batch with zero-variance filtering. A dropped group moves from accepted to filtered, freeing admission capacity. The invariant is submitted = accepted + filtered + running. Without that reclassification, filtered groups could fill the capacity limit and block producers forever.[10]

Filtering changes the sample population and consumes extra prompts. If the epoch runs out before a full useful mini-batch exists, the partial batch is discarded and the epoch ends early. Trained and filtered prompt IDs are both persisted and skipped on resume. Count them separately; an all-zero-reward dataset cannot produce a useful batch under this filter.[10]

The filter compares the reward spread among live trajectories, meaning rows with nonzero loss masks, using a configurable tolerance. Groups with at most one live trajectory are retained by this helper; inspect sample counts and masks to confirm the admitted group contains comparative reward signal.

AReaL, DAPO, and off-policy correction

The systems question is now explicit: can language-model RL keep GPUs busy without letting policy lag erase the training signal? AReaL frames the same trade-off around in-flight generation, workload balance, and staleness control. SkyRL's manager follows that vocabulary, while its own queue and acceptance behavior still need to be read from the local revision.[11]

DAPO supplies a second comparison. Its open system makes dynamic sampling and token-level policy-gradient choices part of the algorithm. SkyRL's sample_full_batch path is an async-native analogue of filter sampling. Establish experimental parity by comparing filtering, reward normalization, rollout policy versions, and evaluation protocol.[12]

Staleness isn't the only source of off-policy behavior. Training and inference can disagree at one nominal weight version because kernels, expert routing, or parallelism differ. SkyRL's correction stack offers truncated , geometric sequence masking, token masking, and routing replay. These controls address residual mismatch; they don't turn an unbounded queue into fresh data.[13]

Use papers as design evidence, then trace the local code that realizes each assumption. A queue can overlap work. Only the lag distribution, reward diversity, and held-out curve can show whether that overlap still produces learnable batches.

Inference and weight synchronization

The opening incident ends at the sampler. After the trainer creates π12\pi_{12}, which process receives it, and which process keeps serving π11\pi_{11} if the transfer is partial? SkyRL separates training and sampling because their hardware and runtimes differ.

The training path commonly uses or Megatron, with JAX available through the Tinker path; generation typically reaches vLLM over HTTP. Training and generation can be colocated on one GPU set or disaggregated across workers.[14][15]

The pinned commit's Linux FSDP and Megatron extras require vllm==0.28.0. Replacing the sampler with today's latest server is a compatibility change: v0.30.0, for example, changed scale-out route registration. Check the recipe's server/router dependencies and actual generation and control endpoints before combining versions. An OpenAI-compatible chat response doesn't demonstrate pause/resume or weight-transfer support.[16][17]

The official inference architecture separates ordinary token traffic from calls on the . Data-plane requests need routing; control-plane calls need to reach every replica. Three pieces carry those jobs:

PieceRolePlane
RemoteInferenceClientSingle HTTP entry point for trainersBoth
VLLMRouterSession-aware load balancer, often wrapping vllm-routerData
vLLM API serversOne replica per engineData plus fan-out control

Generation requests travel through proxy_url: /v1/chat/completions, /v1/completions, tokenize, and related routes. Pause, resume, sleep, wake, and weight-sync endpoints fan out to every server_url. Built-in generators pass the trajectory ID as X-Session-ID, so later turns of query-42_0 stay on one replica and can reuse its prefix cache. sticky_least_loaded is the documented policy for long multi-turn work: the first turn chooses the least-loaded replica, and later turns stay there.[14]

Diagram showing Trainer, RemoteInferenceClient, VLLMRouter, and vLLM replicas.
Trainer, RemoteInferenceClient, VLLMRouter, and vLLM replicas.

Weight synchronization is where "on-policy" becomes an operational claim. In non-colocated mode, SkyRL pauses with /pause?mode=keep, freezes in-flight rollouts with KV state preserved, broadcasts tensors over from trainer rank 0, then resumes.

Current inference docs spell out a four-stage transfer: /init_weight_transfer_engine, /start_weight_update, chunked /update_weights, then /finish_weight_update. Sending a chunk is different from finishing the update; keep generation paused until the selected protocol completes.[14]

In colocated mode it uses CUDA IPC handles plus /sleep and /wake_up so the inference engine can free VRAM during the train step. Project lineage and older docs also describe Gloo and checkpoint-and-load paths; verify the strategy at the revision you deploy. No path removes the need to record policy versions.[14]

The current inference guide also documents delta transfers through a shared filesystem or object store and sharded_rdt transfers pulled through NIXL/RDMA. These have different storage and network requirements from NCCL and IPC. Select and test the backend your deployment actually constructs.[14]

Fully async adds a KV-cache decision. clear_kv_cache_on_weight_sync defaults to false, so a resumed trajectory can retain KV state computed with older weights. generator.use_cache_salt separates prefix-cache reuse across versioned requests; it doesn't make an already-running trajectory fresh. Clearing KV state forces recomputation, but previously sampled actions and buffered groups remain off-policy. It doesn't restore the synchronous sampling schedule.[3]

A sync failure path

Return to step 12. One replica still serves step 11 weights, so routing can give the next group a mixed policy version. Treat that as an evidence problem first, not as a reward problem. A safe recovery sequence is:

  1. Pause new generation if the endpoint supports it.
  2. Record trainer step and sampler version for every replica.
  3. Check transfer logs and replica health.
  4. Retry or the sync operation.
  5. Resume only when all replicas report the expected version.

This operator acceptance contract records the client counter as one signal. Collect sampler-side version evidence for every replica when the client view lacks that coverage. Quarantine a replica with uncertain weights until its sampling probabilities are known. Validate the full update boundary after the RPC succeeds.

Tinker: a stable API over changing hardware

What if the experiment should keep its training calls while the hardware changes underneath? Tinker is a training API from Thinking Machines Lab that presents training and sampling through a service-like interface. SkyRL implements a Tinker-compatible backend so the same style of program can run on local hardware. The unified package carries that work under skyrl; the old skyrl-tx directory records the migration.[15]

The official architecture divides the service into three layers:

LayerResponsibilityBoundary to verify
API (skyrl.tinker.api)FastAPI server, request persistence, future IDsAuth, request identity, and
Engine (skyrl.tinker.engine)Background process that polls, batches, and dispatchesVersion and lifecycle state
Backend (skyrl.backends)FSDP, Megatron, or JAX plus vLLM samplingTensor shapes, optimizer semantics, and weight sync

The lifecycle is easy to test with one update. A user submits an operation and receives a future-like handle. Training calls (forward_backward, optim_step, forward) travel through the engine to GPU workers, while sampling wraps RemoteInferenceClient.

After optim_step, the client must call save_weights_for_sampler() before the next sample if it wants the new policy. Persistent mode also writes a Hugging Face checkpoint; ephemeral mode syncs weights and returns a sampling client without the disk write, which suits a hot RL loop that syncs every batch.[15]

Use save_weights_and_get_sampling_client() for that ephemeral path. A persistent sampler export still isn't the full training-resume artifact: save_checkpoint() preserves model weights, optimizer state, and the learning-rate scheduler. Complete the corresponding operation before relying on its result.[15]

The Tinker architecture guide checked on October 10, 2026 identifies cross_entropy and importance_sampling as validated API losses. Native-trainer registry names such as gspo, sapo, and cispo are not yet wired through that conversion path: the batch lacks the required advantages and old log probabilities. Changing the loss-name string cannot supply those fields. Check the converted batch and an executed update before porting a native-trainer recipe to Tinker.[15]

The boundary can drift. Backend-specific loss support, normalization, checkpoint formats, and model constraints still need integration tests. Don't infer API compatibility merely from a shared loss name. Pin the SkyRL commit, SDK, backend versions, tokenizer, and recipe configuration before comparing runs.

SkyRL-Agent: long-horizon work belongs above the trainer

The same ledger gets longer for SWE (software-engineering) agents, web researchers, and terminal users. One episode may make many tool calls, carry state across turns, and fail because a test, shell, or network service broke. SkyRL-Agent puts task logic, tools, dispatch strategies, and training-backend adapters above the trainer. It can connect to OpenAI-compatible serving such as vLLM, veRL, SkyRL-Train, or Tinker through configuration.[4]

The paper's systems claim is specific. An optimized asynchronous pipeline dispatcher reported a 1.55×1.55\times speedup over naive asynchronous batching by overlapping CPU-bound tool work with GPU generation.

Using that stack, the authors trained SA-SWE-32B from Qwen3-32B with pure RL. The paper reports the base at 24.4% Pass@1 and SA-SWE-32B at 39.4% Pass@1 on SWE-Bench Verified, under a simplified ReAct loop with file-editor and bash tools, 40k context, and 100 max steps. That's a dated paper snapshot, not a live leaderboard. Copy the protocol before comparing the numbers.[4]

The agent layer keeps environment-specific logic close to the task. Browser tools return page observations, code-execution tools return stdout and process status, and finish tools mark episodes complete. The dispatcher can run work asynchronously, while the training backend consumes a normalized trajectory. That division makes a failed test diagnosable without pretending it was a bad gradient.

Application map

ApplicationEnvironment signalSkyRL boundary that matters
Math and reasoningVerifier result or exact answerGroup rewards and zero-variance filtering
Text-to-SQLQuery execution, schema checks, answer matchMulti-turn generator and loss masks
Search and researchRetrieved evidence and citation checksTool observations and trajectory logging
SWE-Bench or Terminal-BenchTests, patch status, process exitSandbox environment, timeout, terminal agent
Browser tasksDOM (Document Object Model) or visual observation, action successAsync tool calls and bounded episode state
Memory agentsRecall and write decisions across turnsStep-wise trajectories and episode completion

The same library can support all six, but the reward contract isn't interchangeable. SQL execution may yield an exact reward; research agents may combine evidence coverage, citation validity, and answer quality; terminal tasks may depend on flaky or expensive tests. Keep those semantics documented next to the environment.

A worked policy update

For a separate binary-reward SQL experiment, sample two queries for the same prompt. A frozen result checker gives the correct query reward 1 and the incorrect query reward 0. Their mean is 0.50.5, giving centered advantages +0.5+0.5 and −0.5-0.5. This binary reward deliberately differs from the earlier shaped turn rewards. Choose the reward contract for the experiment and document how it maps into advantages and loss masks.

With y+y_+ denoting the correct query and y−y_- the incorrect one, the simplified loss encourages that change:

L=−12[(+0.5)log⁡πθ(y+∣x)+(−0.5)log⁡πθ(y−∣x)].L = -\frac{1}{2}\left[(+0.5)\log \pi_\theta(y_+\mid x) + (-0.5)\log \pi_\theta(y_-\mid x)\right].

That equation hides three implementation details:

  1. The group must contain comparable samples for the same prompt.
  2. The reward must be aligned with the environment's true success condition.
  3. The loss mask must identify the response tokens that policy optimization should change.

If both responses receive reward 1, the relative advantages are zero. That can mean an easy prompt or a broken evaluator. Inspect which case applies before changing the ; more samples don't repair a checker that always returns success.

The standard-library example checks mean-centered rewards and the exact counter trace in the figure. It's a small arithmetic model, not a SkyRL training run. Each row is taken just before consumption; accepted counts include groups used by earlier updates.

skyrl_group_and_staleness.py
from math import isfinite
from statistics import fmean

def group_advantages(rewards: list[float]) -> tuple[list[float], bool]:
    if not rewards or not all(isfinite(r) for r in rewards):
        raise ValueError("rewards must be nonempty and finite")
    baseline = fmean(rewards)
    advantages = [reward - baseline for reward in rewards]
    zero_variance = len(set(rewards)) == 1
    return advantages, zero_variance

sql_group = [1.0, 0.0, 1.0, 0.0]
advantages, zero_variance = group_advantages(sql_group)
assert advantages == [0.5, -0.5, 0.5, -0.5]
assert zero_variance is False
assert group_advantages([1.0, 1.0, 1.0, 1.0])[1] is True

print("advantages", advantages)
# (step, consumed group, scheduled step, accepted, running)
trace = [(1, "A", 1, 2, 1), (2, "B", 1, 3, 1),
         (3, "C", 2, 3, 1), (4, "L", 1, 4, 0)]
B, S = 1, 2
for step, group, scheduled, accepted, running in trace:
    consumed = (step - 1) * B
    outstanding = accepted - consumed + running
    assert accepted + running <= (S + step) * B
    assert 0 <= outstanding <= B * (S + 1)
    print(f"step={step} group={group} lag={step-scheduled} outstanding={outstanding}")
assert trace[-1][0] - trace[-1][2] > S

for invalid in ([], [float("nan")], [float("inf")]):
    try:
        group_advantages(invalid)
    except ValueError:
        pass
    else:
        raise AssertionError("invalid reward accepted")
GRPO group and async admission
advantages [0.5, -0.5, 0.5, -0.5]
step=1 group=A lag=0 outstanding=3
step=2 group=B lag=1 outstanding=3
step=3 group=C lag=1 outstanding=2
step=4 group=L lag=3 outstanding=1

The long group L violates the nominal two-step lag budget without violating aggregate capacity. The horizontal axis counts consumption steps. Measure throughput and lag under load to estimate performance for this schedule.

The configuration sketch below exposes the decisions without pretending to be a complete launch recipe. It uses names from SkyRL's documented config model.

skyrl-rl-config.yaml
trainer:
  train_batch_size: 1
  policy_mini_batch_size: 1
  algorithm:
    advantage_estimator: grpo
    policy_loss_type: rollout_is
    zero_variance_filter: true
    dynamic_sampling:
      type: null
  placement:
    colocate_all: false
  fully_async:
    enabled: true
    max_staleness_steps: 2
    num_parallel_generation_workers: 3
    sample_full_batch: true
    clear_kv_cache_on_weight_sync: false
generator:
  batched: false
  n_samples_per_prompt: 4
  use_cache_salt: true
  inference_engine:
    backend: vllm
environment:
  env_class: text2sql

Use the FullyAsyncRayPPOTrainer entrypoint and a revision-matched HTTP generator. rollout_is uses behavior-policy log probabilities; configure the generator to return them. YAML null becomes Python None, unlike the literal string none still shown in some documentation. This sketch omits the model, tokenizer, dataset, placement, and environment, so it isn't a launchable configuration.[10][3]

Check the pinned implementation without a GPU

The downloadable contract probe checks the upstream source bytes against fixed SHA-256 hashes, then executes only the reviewed scheduling classes, buffer-drain method, and reward-stamping method. It avoids the GPU-dependent module imports. Run it from the article directory; --fetch downloads the two public source files into a temporary source directory.

verify_pinned_skyrl_contracts.sh
uv run assets/verify_pinned_contracts.py --fetch

The recorded CPU receipt contains eight passing checks, including a blocked producer released by filtering, epoch accounting, checkpoint capacity restoration, the long-group trace, buffer exhaustion, and reward truncation/EOS branches. Those are executed method checks, not a full upstream test-suite pass. A custom chat-template branch returns the final scalar reward instead of the per-token vector, so a generator extension must test its selected path.[8]

No CUDA model, Ray cluster, vLLM HTTP server, real SQL environment, weight transfer, or learning curve was executed for this review. The simulated trainer still needs inference endpoints and skips actual optimization and broadcast, so distributed updates remain untested. Before a real experiment, run the revision's small synchronous recipe, verify a save/resume round trip, then compare async against that baseline at equal evaluated prompts and GPU-hours.

Strengths and weaknesses

A component count doesn't say whether query-42_0 stayed aligned. A run does. The first table asks what each seam buys. The evidence column names the measurement that can confirm it.

Strengths

StrengthWhy it mattersEvidence to collect
Modular trainerNew algorithms and execution plans avoid task rewritesGenerator and backend diffs stay local
Real environment loopRewards can come from tests, SQL execution, or toolsReplayable observations and verifier output
Multiple placement modesColocated and disaggregated training fit different hardwareGPU map, sync latency, and queue metrics
Async supportLong tool calls can run without idling every workerThroughput plus staleness distribution
HTTP inference splitRouting and weight sync can be debugged separatelySession stickiness, pause mode, replica versions
Tinker compatibilityRecipes can target a stable API over local hardwareAPI call trace and checkpoint resume
Agent layerLong-horizon tools and backends share a dispatcher contractPer-tool latency, episode completion,
Inspectable configConfiguration and interfaces expose decisionsSmall experiment diff and reproducible config

The useful sequence is incremental: start with synchronous GRPO, add a custom generator, then test async generation while preserving output fields. Each experiment changes one boundary at a time, so an ablation can point to a seam instead of a pile of infrastructure changes.

Weaknesses and sharp edges

WeaknessWhy it bites
Many moving servicesRay, inference servers, environments, trackers, and storage can fail independentlyHealth checks, versioned configs, and run manifests
Async freshness riskFast generation can outrun training and produce stale groupsCap buffer, measure the lag distribution, alert on sustained drift
Backend divergenceFSDP, Megatron, JAX, and vLLM differ in kernels and numericsTest one backend path end to end before comparing; watch logprob diffs
Reward quality dominatesA fast trainer optimizes a bad verifier fasterFrozen evaluator slices and reward audits
Reorganization churnskyrl-train and skyrl-tx paths moved into skyrl/Pin commits and follow official migration notes
Heavy environment setupGPU, CUDA, Ray, and model artifacts are expensiveUse the simulated fully async trainer or a small smoke environment first
Unsupported combinationsFully async rejects batched generate, colocation, and sync dynamic samplingRead config validation errors before changing code

These edges define the admission bar for a serious RL experiment: state, traceable policy versions, bounded queues, and an explicit stop when rewards or sync health become untrustworthy. The operator section turns that bar into evidence you can collect before burning more GPU hours.

Research papers and design lineage

The core path is now complete. Keep the papers as a second reading layer: they explain why a workload matters and what was measured, while the code shows which assumptions became interfaces and which remain configuration constraints.

Paper or sourceDesign lesson to carry into code reading
SkyRL-v0 and the project repositoryLong-horizon agents need environment integration alongside a loss function.[1]
SkyRL-AgentTools, dispatch, and backend adapters can be a reusable layer for multi-turn agents.[4]
DAPODynamic sampling and scalable policy-gradient systems make data selection part of algorithm design.[12]
AReaLFully async reasoning systems need explicit overlap and staleness accounting.[11]
SkyRL overviewTrainer, Generator, InferenceEngine, Environment, and Controller are the core conceptual interfaces.[2]
SkyRL inference architectureControl plane, data plane, routing, placement, and sync should be debugged separately.[14]
SkyRL Tinker architectureA stable SDK can front multiple engines and checkpoint lifecycles.[15]
SkyRL off-policy correctionTrain/infer mismatch and async lag are different ratios and need different knobs.[13]

This table supplies research context rather than a leaderboard. DAPO and AReaL cover related systems, while SkyRL provides a framework for testing environment and execution-plan variants. Compare exact task, base model, reward, sampling, and update settings before importing a result.

Admit a run and recover it

Before launching a serious SkyRL run, write its evidence contract. If one answer is unknown, use a smaller smoke job before spending more GPU hours. The order matters: trust the reward, identify the policy, bound overlap, then rehearse recovery.

Gate 1: make reward traceable

Define one episode and its termination condition. Record which tokens receive policy loss and whether reward is scalar, per-turn, or per-token. Then ask whether the evaluator can give every sample the same score and whether tool outputs are reproducible under a fixed seed and environment snapshot. A zero-variance group or nondeterministic tool can flatten a run before optimization gets a chance to help.

Gate 2: name every weight owner

Write down which process owns training, reference, and sampler weights, whether training and generation are colocated or disaggregated, and which inference and weight-sync backends are selected. After every sync, require every serving replica to report the same policy version. Pin model, tokenizer, CUDA, Ray, and dependency versions so a replay has a meaningful environment.

Gate 3: bound async work

Set mini-batch size, generation-worker count, and max_staleness_steps together. Calculate completed-group plus in-flight headroom as B(S+1)B(S+1), choose the pause mode (keep, abort, or wait), and check whether use_cache_salt isolates prefix-cache blocks across policy versions. Decide which trajectories are filtered as lag rises, how generation failures surface, and which metric stops the run automatically.

Gate 4: keep a recovery packet

A replayable trajectory joins prompt, actions, observations, rewards, and policy version. Logs separate environment latency from inference latency. Require checkpoint saves to be atomic and resumable, and keep a rollback path for reward or sync regressions. Finally, name the offline slice that must pass before online gains count as learning.

🎯 Production tip: Log trajectory ID, scheduled policy step, consumed policy step, reward components, stop reason, and environment revision together. A throughput chart without those joins fails to explain a bad update.

When the extra seams pay off

Choose this stack when the research question includes an environment, a changing rollout policy, or a nontrivial training and inference plan. Tool-use agents, verifier-backed reasoning, Text-to-SQL, terminal tasks, and sync-versus-async experiments all cross those boundaries.

A smaller trainer fits a static supervised dataset with no environment or rollout control. SkyRL's interfaces add coordination cost, so their value starts when one of those boundaries is part of the question.

There is a second fork when policy or data must stay outside your GPU cluster. SkyRL-Tinker offers a service-like API with local hardware control, but its compatibility boundary still needs a pinned integration test.[15]

The practical handoff is hybrid: prototype reward and environment logic in a synchronous local run, validate trajectory fields and evaluator slices, then move to disaggregated or fully async execution after a baseline and staleness budget exist. That order lets a later throughput win answer a useful question: did the system learn more, or did it only move more stale tokens?

Keep query-42_0 aligned from the observation mask, through the staleness counters, to the replica that still serves the previous step. If a chart can't do that, it isn't a learning result.

Review an extension before running it

Evaluation rubric

  • Foundational: Trace one response through token IDs, observation masks, reward placement, and the final stop reason without treating a tool observation as a sampled action.
  • Intermediate: Reconstruct accepted, running, filtered, and consumed counts, including a long group whose scheduling lag exceeds the aggregate budget.
  • Advanced: Separate backend/API compatibility, sampler-version evidence, cache behavior, and held-out learning outcomes when accepting an async change.

Follow-up questions

Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.A multi-turn SQL trajectory includes sampled tokens, a database ERROR observation, and a repair response. Which field prevents the trainer from assigning policy loss to the observation?
2.A rollout is scheduled at global step 4 and consumed at step 6. Under SkyRL's definition, what staleness does the trainer record?
3.A fully async run uses mini-batch size B=1, current_global_step=2, and max_staleness_steps S=2. What aggregate producer capacity does the documented bound allow?
4.A new browser environment produces reward zero for every sample in synchronous GRPO. What should the engineer inspect before changing the optimizer?
5.SkyRL sends generation through a load-balanced proxy and has three sampler replicas. A new policy must be installed before the next rollout. Where should control operations go?
6.Which evidence most strongly supports a completed weight-sync boundary?
7.Why does the default multi-turn generator keep sampled token IDs and append tokenized observations instead of rebuilding the whole chat from strings?
8.A zero-variance group was accepted, then dropped by sample_full_batch. Which counter change prevents it from permanently occupying admission capacity?
9.An async trajectory pauses after sampling some actions. Clearing KV state during weight sync changes which property?
10.A team wants a stable user-facing training API while switching between local FSDP, Megatron, and hosted backends. Which SkyRL boundary is intended for that change?

10 questions remaining.

References

SkyRL

NovaSky AI · 2026

https://github.com/NovaSky-AI/SkyRL

SkyRL Overview

NovaSky AI · 2026

https://docs.skyrl.ai/docs/getting-started/overview

Fully Asynchronous Training

NovaSky AI · 2026

https://docs.skyrl.ai/docs/tutorials/fully_async

SkyRL-Agent: Efficient RL Training for Multi-turn LLM Agent

Cao, S., Li, D., Zhao, F., et al. · 2025 · arXiv

https://arxiv.org/abs/2511.16108

Developing SkyRL

NovaSky AI · 2026

https://docs.skyrl.ai/docs/getting-started/development

SkyRL Apache License 2.0

NovaSky AI · 2026

https://github.com/NovaSky-AI/SkyRL/blob/ee7ab9927d5d0a3436dd98fd6b955fc9718d0260/LICENSE

SkyRL Generator Contract (955c3e2)

NovaSky AI and Contributors · 2026

https://github.com/NovaSky-AI/SkyRL/blob/955c3e23bf8939b28a9d6c424308a762dcb8a2e7/skyrl/train/generators/base.py

SkyRL Gym Generator (955c3e2)

NovaSky AI and Contributors · 2026

https://github.com/NovaSky-AI/SkyRL/blob/955c3e23bf8939b28a9d6c424308a762dcb8a2e7/skyrl/train/generators/skyrl_gym_generator.py

SkyRL-Gym Generator

NovaSky AI · 2026

https://docs.skyrl.ai/docs/tutorials/skyrl_gym_generator

SkyRL Fully Asynchronous Trainer (955c3e2)

NovaSky AI and Contributors · 2026

https://github.com/NovaSky-AI/SkyRL/blob/955c3e23bf8939b28a9d6c424308a762dcb8a2e7/skyrl/train/fully_async_trainer.py

AReaL: A Large-Scale Asynchronous Reinforcement Learning System for Language Reasoning

Fu, W., et al. · 2025 · arXiv

https://arxiv.org/abs/2505.24298

DAPO: An Open-Source LLM Reinforcement Learning System at Scale

Yu, Q., et al. · 2025 · arXiv

https://arxiv.org/abs/2503.14476

Off-Policy Correction at bce9ee9

NovaSky AI · 2026

https://github.com/NovaSky-AI/SkyRL/blob/bce9ee9a80fbd262db44c79d5af12291ced5492d/docs/content/docs/algorithms/off_policy_correction.mdx

SkyRL Inference Architecture

NovaSky AI · 2026

https://docs.skyrl.ai/docs/getting-started/inference_architecture

SkyRL-Tinker Architecture

NovaSky AI · 2026

https://docs.skyrl.ai/docs/tinker/architecture

SkyRL Dependencies (955c3e2)

NovaSky AI and Contributors · 2026

https://github.com/NovaSky-AI/SkyRL/blob/955c3e23bf8939b28a9d6c424308a762dcb8a2e7/pyproject.toml

vLLM v0.30.0 Release

vLLM Project · 2026

https://github.com/vllm-project/vllm/releases/tag/v0.30.0

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.