Implementing Custom Rollout Workflows for Agentic RL in AReaL
To implement a custom rollout workflow for agentic RL in AReaL, subclass the abstract RolloutWorkflow class from areal/api/workflow_api.py and implement the asynchronous arun_episode method to orchestrate tokenization, LLM generation, reward calculation, and tensor packaging.
AReaL (inclusionai/areal) separates RL-from-Human-Feedback (RLHF) into modular components, with the rollout workflow serving as the critical bridge between inference and training. By implementing custom rollout workflows, you define exactly how raw prompts transform into training-ready tensor bundles containing inputs, masks, log-probabilities, and rewards.
Understanding the Rollout Workflow Architecture
The architecture centers on the abstract RolloutWorkflow API defined in areal/api/workflow_api.py. This contract requires implementing an asynchronous method that coordinates the entire episode generation pipeline from input data to batched tensors.
Core Components
The workflow interacts with several key interfaces:
RolloutWorkflow– The abstract base class requiringasync def arun_episode(self, engine: InferenceEngine, data: dict[str, Any]) -> dict[str, torch.Tensor]InferenceEngine– Providesagenerate(request)to produceModelResponseobjects fromModelRequestinputsModelRequest/ModelResponse– Structured containers for token IDs, logits, log-probabilities, versioning metadata, and optional vision dataAsyncRewardWrapper– Wraps synchronous reward callables for non-blocking async execution- Tracing Utilities –
@trace_sessionandsession_context()decorators fromareal/utils/perf_tracer.pyfor performance profiling
Execution Flow
A standard rollout follows five distinct phases, as implemented in areal/workflow/rlvr.py:
- Input preparation – Convert raw data into
input_idsusing configurable tokenization - Request construction – Build
ModelRequestwith generation hyperparameters (gconfig) and tokenizer metadata - Generation – Execute
engine.agenerate(req)inside a tracedgeneratephase - Reward computation – Decode tokens and invoke the async reward function
- Tensor packaging – Assemble batched tensors including
input_ids,loss_mask,logprobs,versions,attention_mask, andrewards
Step-by-Step Implementation Guide
Follow these steps to create a production-ready custom workflow:
- Create a module under
areal/workflow/(e.g.,custom_task.py) - Subclass
RolloutWorkflowfromareal/api/workflow_api.py - Implement
arun_episodewith the signatureasync def arun_episode(self, engine: InferenceEngine, data: dict) -> dict[str, torch.Tensor] - Factor helper methods using
@trace_session("reward")for reward calculation and@session_context()for generation phases - Return batched tensors with an extra batch dimension (
unsqueeze(0)) for trainer compatibility
Minimal Implementation Example
Here is a complete custom workflow implementation:
# areal/workflow/custom_task.py
import uuid
import torch
from transformers import PreTrainedTokenizerFast
from areal.api.workflow_api import RolloutWorkflow
from areal.api.engine_api import InferenceEngine
from areal.api.io_struct import ModelRequest
from areal.utils.logging import getLogger
from areal.utils.perf_tracer import trace_session, session_context
logger = getLogger("CustomTaskWorkflow")
class CustomTaskWorkflow(RolloutWorkflow):
"""Single-generation workflow with scalar reward computation."""
def __init__(self, reward_fn, tokenizer: PreTrainedTokenizerFast, gconfig):
self.reward_fn = reward_fn
self.async_reward_fn = AsyncRewardWrapper(reward_fn)
self.tokenizer = tokenizer
self.gconfig = gconfig.new_with_stop_and_pad_token_ids(tokenizer)
@trace_session("reward")
async def _compute_reward(self, resp, prompt_str, task_data):
completions = self.tokenizer.decode(resp.output_tokens)
return await self.async_reward_fn(prompt_str, completions, **task_data)
@session_context()
async def _generate(self, engine: InferenceEngine, req: ModelRequest, prompt_str, task_data):
resp = await engine.agenerate(req)
reward = await self._compute_reward(resp, prompt_str, task_data)
return resp, reward
async def arun_episode(self, engine: InferenceEngine, data: dict):
# Tokenize prompt
input_ids = self.tokenizer.apply_chat_template(
data["messages"], tokenize=True, add_generation_prompt=True
)
req = ModelRequest(
rid=uuid.uuid4().hex,
input_ids=list(input_ids),
gconfig=self.gconfig.new(n_samples=1),
tokenizer=self.tokenizer,
)
prompt_str = self.tokenizer.decode(input_ids)
# Generate and compute reward
resp, reward = await self._generate(engine, req, prompt_str, data)
# Build output tensors
seq = resp.input_tokens + resp.output_tokens
loss_mask = [0] * resp.input_len + [1] * resp.output_len
logprobs = [0.0] * resp.input_len + resp.output_logprobs
versions = [-1] * resp.input_len + resp.output_versions
result = {
"input_ids": torch.tensor(seq, dtype=torch.int32),
"loss_mask": torch.tensor(loss_mask, dtype=torch.int32),
"logprobs": torch.tensor(logprobs, dtype=torch.float32),
"versions": torch.tensor(versions, dtype=torch.int32),
"rewards": torch.tensor(reward, dtype=torch.float32),
"attention_mask": torch.ones(len(seq), dtype=torch.bool),
}
# Add batch dimension for trainer compatibility
return {k: v.unsqueeze(0) for k, v in result.items()}
Critical implementation details:
- All return tensors must include a batch dimension of 1 using
unsqueeze(0) - Use
uuid.uuid4().hexfor unique request IDs inModelRequest - Wrap synchronous reward functions with
AsyncRewardWrapperto prevent blocking - Maintain the exact tensor schema:
input_ids,loss_mask,logprobs,versions,rewards,attention_mask
Implementation Patterns from Built-in Workflows
AReaL provides three reference implementations demonstrating different agentic RL patterns:
Single-Turn RL with RLVRWorkflow
areal/workflow/rlvr.py implements the canonical text-only RL-from-Verification workflow. It demonstrates standard tokenization, single-pass generation, and straightforward reward attribution. Use this as the baseline for text-based tasks with immediate reward signals.
Multi-Attempt Logic with MultiTurnWorkflow
areal/workflow/multi_turn.py extends the base pattern with retry loops. The workflow continues generating attempts until achieving a positive reward or hitting attempt limits. This pattern is essential for agentic tasks requiring iterative refinement or tool use with success verification.
Vision-Enabled Rollouts with VisionRLVRWorkflow
areal/workflow/vision_rlvr.py shows how to incorporate multimodal inputs. It utilizes AutoProcessor for image preprocessing and populates the multi_modal_input field in ModelRequest. This workflow handles vision-language tasks where rewards depend on generated descriptions of visual content.
Summary
- Subclass
RolloutWorkflowfromareal/api/workflow_api.pyand implementarun_episodeto define custom agentic RL logic - Return batched tensors with shape
(1, seq_len)usingunsqueeze(0)for compatibility with AReaL's training loop - Orchestrate four phases: input tokenization,
engine.agenerate()execution, async reward computation, and tensor packaging - Leverage tracing decorators (
@trace_session,@session_context) fromareal/utils/perf_tracer.pyto maintain observability standards - Reference built-in workflows (
RLVRWorkflow,MultiTurnWorkflow,VisionRLVRWorkflow) for single-turn, multi-attempt, and vision-enabled patterns
Frequently Asked Questions
What tensor schema must arun_episode return for AReaL training?
The method must return a dictionary containing six specific tensors: input_ids (int32), loss_mask (int32), logprobs (float32), versions (int32), rewards (float32), and attention_mask (bool). Each tensor must include a batch dimension of 1, achieved by calling unsqueeze(0) on the sequence-level tensors before returning.
How do I handle synchronous reward functions in the async workflow?
Wrap synchronous reward functions using AsyncRewardWrapper before assignment to self.async_reward_fn. This wrapper ensures the reward computation runs in a separate thread pool, preventing blocking of the async event loop during generation batches.
Can custom workflows support multimodal inputs like images?
Yes. Following the pattern in areal/workflow/vision_rlvr.py, import AutoProcessor alongside your tokenizer, preprocess images through the processor, and pass the resulting tensors via the multi_modal_input parameter when constructing ModelRequest objects.
Where should I place tracing decorators for optimal profiling?
Apply @session_context() to generation wrapper methods and @trace_session("reward") to reward calculation methods. These decorators integrate with AReaL's performance tracer in areal/utils/perf_tracer.py, logging latency metrics and phase durations to the stats tracker automatically.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →