Data engineering recipes#
areal.v2.data_engineering runs bounded foreground data jobs. It provides recipe
execution, public-task calibration, deduplication, provenance, and verified dataset
snapshots. This is a default task-data workflow; task projects can compose multiple jobs
or use other workflows when their requirements differ. A research controller chooses the
objective and recipe; the recipe supplies domain-specific generation, cleaning, scoring,
selection, and training projection.
Run the TIR example#
From the repository root with AReaL’s normal dependencies installed:
python3 -m areal.v2.data_engineering \
--config examples/data_engineering/tir/data_generation.yaml \
--output /tmp/tir-data-job \
--export-hf /tmp/tir-training-data
For a new job, both output directories must be new. Use --resume to continue an
existing data job. This example generates exact-answer math tasks without a model, GPU,
or inference service. AReaL’s package imports still require its normal Python
dependencies. The final stdout line contains dataset_id, dataset_path, and, when
requested, training_dataset_path.
The TIR recipe has eight families and two structures per family. Its training view
contains task_id, messages, and an exact boxed answer. It produces task data;
training rollouts, tokenization, and RL/SFT/OPD tensors are created downstream.
Python API and execution protocol#
import asyncio
from areal.v2.data_engineering import DataJobConfig, run_data_job
config = DataJobConfig(
recipe="examples.data_engineering.tir.data_recipe:create_recipe",
target_count=64,
purpose="joint",
seed=42,
params={"candidate_count": 128},
)
snapshot = asyncio.run(run_data_job(config, "/tmp/tir-python-job"))
rows = snapshot.training_rows()
Inside an existing async controller, use await run_data_job(...). A factory is an
importable module:callable, invoked as factory(**params), returning a DataRecipe.
The public Python protocols are exported from areal.v2.data_engineering:
Type or method |
Contract |
|---|---|
|
Stable |
|
|
|
Yield a bounded batch of samples; the context contains seed, batch index, target, purpose, and input file paths |
|
Return a cleaned sample or |
|
Use the private reference to return a finite score in |
|
Return |
|
Project one selected task to one domain-specific JSON training row |
|
Optional recipe/calibrator method declaring helper code and external files that affect the result |
The executor runs generate → clean → deduplicate/exclude → calibrate → select. Cleaned
candidates and their scores accumulate across batches. Only InsufficientDataError
requests another candidate batch; other errors fail the job after any configured
transient retries. Generic max_batches and max_candidates bound the search; domain
parameters stay in params. A recipe may read raw data through context.inputs instead
of synthesizing it. Inputs and exclusions are files, resolved relative to the config
file by the module entry point. Paths inside plugin params remain plugin-owned; the
supplied calibration adapters resolve them against the process working directory.
purpose is harness, model, or joint. It is recorded and passed to the recipe;
recipes decide how it affects generation and selection. It does not automatically infer
whether a failure came from model capability or Harness behavior. Independent dev/test
sets and the decision to adopt a Harness/model remain with the caller.
Identical task IDs must identify identical cleaned content. Duplicate public inputs with
conflicting references fail the job. Exact JSON input duplicates are removed; semantic
or domain-specific normalization belongs in clean(). Exclusion JSONL files need
top-level task_id fields; use prior snapshots’ public.jsonl or a previous job’s
work/batch-N-public.jsonl to exclude previously selected or probed tasks.
Parallel execution and recovery#
Add an execution section to a job configuration:
execution:
max_workers: 4
batch_size: 256
backend: process
max_retries: 2
retry_delay: 1.0
run_data_job starts a dedicated coordinator thread with its own event loop. Global
coordination, source hashing, SQLite operations, and snapshot I/O execute there, leaving
the caller’s event loop responsive. Factories create calibrators inside that job loop;
they must not reuse async resources tied to the caller’s loop. The SQLite connection is
created and used in its owning coordinator thread.
Cancelling the caller task signals the job and cancels pending async calibration. The call waits for worker shutdown, database closure, and final state persistence; repeated cancellation does not abandon cleanup. Synchronous plugin/storage calls cannot be forcibly stopped: cancellation takes effect when they return, before starting the next stage or chunk. A snapshot already atomically published is retained and verified on resume. This does not make a non-returning plugin interruptible.
max_workers defaults to 1, batch_size to 256, backend to thread, and
max_retries to 0. thread suits blocking I/O; process uses spawned Python workers
for CPU-bound generation, cleaning, scoring, ranking, and training projection. Process
workers require importable factories and picklable raw samples. Scripts that use process
workers must call asyncio.run(...) under an if __name__ == "__main__": guard.
Deduplication and the final global constraint merge run in the coordinator. Async
calibration is split into at most max_workers concurrent calls, each with at most
batch_size public tasks; repeats stay inside the calibrator protocol.
python3 -m areal.v2.data_engineering \
--config examples/data_engineering/tir/data_generation.yaml \
--output /tmp/tir-data-job --resume
Python callers use await run_data_job(config, output, resume=True). Recovery requires
the same configuration, declared input/exclusion files, recipe/calibrator source files,
and executor code. Changed identities fail before execution starts. A completed job
returns its verified existing snapshot. HF export still requires a new destination; omit
--export-hf when that dataset has already been exported.
The local work/checkpoints.sqlite3 database atomically commits each cleaning,
calibration, and projection chunk. Candidate payloads, deduplication indexes, and probe
results and ranked candidates stay on disk; only bounded worker chunks are prefetched.
Snapshot publication and verification stream files. Selection receives a disk-backed
Sequence[DataSample] and score Mapping: use iteration for large pools, since
repeated indexed lookups/slicing can be expensive. Recipe code can still materialize its
own pool; training_rows() and HF export also intentionally materialize rows. The
executor ranks TIR candidates in parallel and selects through disk indexes; calling its
standalone select() method still materializes a sorted list.
Use a local filesystem for the SQLite WAL database and process lock. This is single-host
execution, not a Ray/Slurm distributed job. The lock is released on process death, so
SIGKILL does not require deleting a stale lock file. Derived
work/batch-N-{candidates,public}.jsonl files are rebuilt from checkpoints.
Recipe methods must be deterministic from their arguments and declared inputs; cleaning/projection workers create their own instances. No mutable instance state is saved. If generation was interrupted, it replays from the start of that batch, skipping committed cleaning chunks; completed generation batches are not replayed. Calibration must support concurrent calls when more than one worker is configured.
Only OSError (including connection failures) and TimeoutError retry, using
exponential backoff capped at 60 seconds. Validation errors and cancellation do not
retry. An Attempt.failure_reason is a completed calibration result, not an exception
to retry. Persisted chunks are reused, but failed or uncommitted chunks may execute
again. External services should use task/repeat identities for idempotency; the executor
does not promise exactly-once external side effects.
Parallel generation, scoring, and global selection#
Existing recipes remain valid: without the optional hooks, generation and selection use their original coordinator implementation. Scoring always runs in worker chunks. To parallelize generation, implement both methods:
Hook |
Contract |
|---|---|
|
Yield deterministic |
|
Yield at most |
Each generation worker constructs its own recipe, generates and cleans a shard, and
streams the retained/null rows to work/generation-B-S.jsonl. The adjacent JSON
metadata records its range, row count and digest. Complete verified files are reused on
resume, even if the database commit was interrupted. The coordinator imports rows in
bounded batch_size chunks and applies global deduplication in shard order. Recipes
choose shard sizes; larger shards use fewer files but have a coarser retry boundary.
generate(context) should concatenate the same deterministic shard streams.
Calibration responses are validated and checkpointed before scoring. Grading failures retry/recover from the stored responses without repeating completed model requests. Workers instantiate their own scoring recipes, so scores must not depend on mutable state established by generation or calibration.
To parallelize ranking, implement all three methods:
Hook |
Contract |
|---|---|
|
Return |
|
Return |
|
Return final task IDs/statistics, or raise |
A RankedCandidate contains finite floating-point priority, string tie_breaker,
group, bucket, and JSON metadata. Workers independently check eligibility and sort
local chunks by (priority, tie_breaker, task_id). SQLite indexes retain the global
ordering; already committed ranking chunks are reused. A candidate’s ranking must stay
fixed as later batches arrive; it cannot depend on the evolving candidate pool.
Implementations with adaptive global policies can retain the original select.
The coordinator first fills group minima in the policy’s insertion order, then fills
remaining slots globally by rank, checking bucket caps and duplicate IDs throughout.
This final merge is serial and deterministic; it is a greedy policy, not a solver for
arbitrary overlapping constraints. Recipes can call select_ranked_candidates with an
in-memory ordered-row provider to share exactly the same merge algorithm. The TIR
example uses 16 independently seeded recipe streams, family minima, recipe caps and a
final borderline-case check. Its candidates are invariant to worker count, but this
generator version does not reproduce the previous generator’s candidates; recorded
calibration reports must match the actual candidate/source identities.
Calibration protocol#
A calibration factory returns an async Calibrator:
async def __call__(self, tasks, context):
# tasks: Sequence[PublicTask]; context: CalibrationContext
# Process public tasks and report implementation-specific provenance.
return CalibrationBatch(tuple(attempts), provenance={"method": "my-calibrator-v1"})
An Attempt has task_id, zero-based repeat, JSON-serializable response, and
optional failure_reason. Every requested task/repeat pair must be returned exactly
once. The generic protocol requires no model or Harness binding. A nonempty failure
reason produces a None score; an incorrect completed response receives the recipe’s
ordinary score. Backend mutation of public request objects does not mutate retained
candidates.
The calibration backend receives no DataSample.reference. The executor grades raw
responses with the recipe’s private reference. This is a data contract, not a security
sandbox: recipe and calibrator code is trusted. Implementation-specific dependencies and
version checks belong in the calibrator. Optional JSON provenance is preserved per
batch; for example a rule version, source revision, or evaluated model/Harness identity.
The TIR adapter validates the latter inside its own implementation.
For TIR, use two attempts per task. Its numeric verifier requires a final \boxed{...}
and supports integers, decimal values, rational strings, and numeric LaTeX fractions.
Expressions such as 1+2 are not evaluated. Selection drops incomplete and
twice-correct tasks, prioritizes once-correct tasks, and then uses twice-wrong tasks. It
enforces family coverage and the configured recipe cap. Without calibration, it selects
generated tasks without claiming model difficulty calibration.
Reuse existing TIR evaluation reports#
recipe: examples.data_engineering.tir.data_recipe:create_recipe
purpose: joint
seed: 42
target_count: 512
max_batches: 8
max_candidates: 8192
params:
candidate_count: 1024
minimum_one_pass: 128
maximum_per_recipe: 48
calibration:
implementation: examples.data_engineering.tir.data_calibration:recorded_responses
repeats: 2
params:
model_ref: /path/to/the/evaluated/checkpoint
harness_ref: actual-harness-id
reports:
- [/path/to/batch0-repeat0.json, /path/to/batch0-repeat1.json]
backend_model_digest: actual-model-digest
verifier_digest: actual-verifier-digest
Supply reports for every batch that may be needed. Reports must cover the generated task IDs and match the requested model, Harness, model digest, and verifier digest. Repeats must have distinct trial IDs and the same task set/executable identity. Stored scores are ignored and responses are graded again. Report and source digests enter the snapshot provenance. Reuse requires matching the original generator seed and candidate count; this adapter does not run an evaluation or start SGLang.
Artifacts and training integration#
job.json records running/succeeded/failed/cancelled state. config.json,
progress.json, and per-batch files under work/ retain execution evidence.
Publication occurs only after successful selection and validation, by renaming a staging
directory on the same filesystem. Existing output directories require --resume. Jobs
run in the foreground; interrupting the module cancels it. Recovery continues from
committed chunks without a background daemon.
The dataset directory contains:
public.jsonl: selected public tasks.references.jsonl: aligned private references.train.jsonl: the recipe’s training view.probes.jsonl: raw calibration responses, failure reasons, and recomputed scores.report.json: generation, cleaning, exclusion, and selection counts.manifest.json: content identity, file digests, config, recipe/backend code digests, declared source digests, and implementation-specific calibration provenance.
Job directories and dataset files are owner-restricted. load_snapshot(path) and
snapshot.training_rows() verify file digests and row alignment. Snapshots are
immutable by convention and identified by content; permissions do not make them
write-proof. Factories should declare imported helper code and non-config input files
with source_files(). Declared inputs are checked again before publication.
Use the explicit materialization adapter for existing training loaders:
from areal.dataset import get_custom_dataset
from areal.v2.data_engineering import export_hf_dataset, load_snapshot
snapshot = load_snapshot("/tmp/tir-data-job/dataset")
path = export_hf_dataset(snapshot, "/tmp/another-tir-training-data")
dataset = get_custom_dataset(path=str(path), type="rl")
The export includes areal-source.json with the source dataset identity. Choose a
workflow that consumes the recipe’s columns. For SFT or OPD, provide the appropriate
projection/tokenization/teacher-processing adapter; TIR’s task-only view is not an SFT
conversation or a tokenized RL trajectory.