rfgen.executors¶
Concrete execution backends for Phase 1 sharded IQ generation. Local execution is synchronous; PySpark is the sole supported distributed backend. Dask and Ray are deferred.
Every concrete class subclasses
BaseDistributedExecutor and is
selected at runtime by config (executor.name: ...). Cloud-specific subclasses
live in optional extras and lazy-import their vendor SDK; importing
rfgen.executors itself never pulls a cloud SDK. See
Reference / Cloud Backends for which subclass ships in
which extra.
Warning
Most executor classes on this page remain pre-implementation proposals. The Layer 19 run-control reference is implemented and has a deliberately narrower, in-memory scope. It is not a durable executor or cloud control plane.
Module summary¶
from rfgen.config import GenerationConfig
from rfgen.executors import SparkExecutor, ShardSpec
from rfgen.orchestration import build_shard_fn
cfg = GenerationConfig.model_validate({"run": {"run_id": "demo"}, "storage": {"path": "gs://b/d/v1"}})
cfg_hash = "sha256:demo-config"
dataset_uri = "gs://b/d/v1"
shard_fn = build_shard_fn(cfg)
executor = SparkExecutor.from_config({"app_name": "rfgen-demo", "max_executors": 16})
shards = [ShardSpec.for_shard(shard_index=i, num_samples=4096,
config_hash=cfg_hash, output_uri=dataset_uri)
for i in range(64)]
handle = executor.submit(shard_fn, shards)
result = executor.wait(handle)
print(result.succeeded_shards, result.failed_shards, result.elapsed_s)
Every executor has the same surface: submit(shard_fn, shards) returns an
ExecutorRunHandle; wait(handle) blocks and returns a RunResult;
cancel(handle) aborts. What differs is the cluster type, credential chain,
and how shards are dispatched to workers.
Run-control protocol¶
rfgen.run_control supplies the implemented, strict reference contract for
submitting, observing, pausing, resuming, cancelling, and reconciling a run.
Its InMemoryRunController
uses an injected clock and process-local storage. It makes record shapes,
state transitions, idempotency, pagination, and reconciliation executable;
it does not submit Spark work, persist state across a process restart, or
call a cloud provider. Durable control-plane adapters are deferred to Layer
29.
All V1 records are frozen Pydantic v2 models with strict parsing and no
unknown fields. Records that carry a *_sha256 field use a lowercase SHA-256
digest of their RFC 8785 canonical JSON representation, excluding that
record’s self-hash field. Timestamps must be UTC.
Public records and enums¶
API |
Contract |
|---|---|
Closed lifecycle: |
|
Append-only lifecycle, unit, and reconciliation event kinds. Lifecycle events encode an allowed edge; unit events do not change the run lifecycle. |
|
Sorted, unique retryable codes; 1 to 100 attempts; finite positive backoff bounds and multiplier at least one. |
|
Run UUID, manifest revision, sorted unique unit IDs, retry policy, lease TTL of 30 to 86,400 seconds, in-flight limit of 1 to 10,000, and a self-hash. |
|
Immutable run identity, specification hash, hashed idempotency key, controller backend, creation time, and self-hash. |
|
Immutable observed lifecycle state, sequence number, unit counts, lease, optional failure, observation time, and self-hash. Counts must sum to the total. |
|
Ordered audit event with actor, manifest revision, optional unit attempt, provider handle or billing ID, and self-hash. Failure events require a RunErrorV1; other event kinds must not carry one. |
|
Immutable event page bound to a 24-hour snapshot, including a signed opaque continuation token when another page exists. |
|
Hash-verified report containing rows sorted by run ID, unit ID, and attempt. Each row records the provider observation, action, and before/after unit state. |
Controller contract¶
class rfgen.run_control.InMemoryRunController¶
class InMemoryRunController:
def __init__(
self,
*,
now: Callable[[], datetime] | None = None,
backend: str = "memory",
) -> None: ...
def submit(
self, spec: RunSpecV1, *, idempotency_key: str, deadline: datetime
) -> RunHandleV1: ...
def status(self, handle: RunHandleV1, *, deadline: datetime) -> RunStatusV1: ...
def pause(
self, handle: RunHandleV1, *, expected_state: Literal[RunState.RUNNING],
deadline: datetime,
) -> RunStatusV1: ...
def resume(
self, handle: RunHandleV1, *, expected_state: Literal[RunState.PAUSED],
deadline: datetime,
) -> RunStatusV1: ...
def cancel(
self, handle: RunHandleV1, *, expected_state: RunState,
deadline: datetime,
) -> RunStatusV1: ...
def events(
self, handle: RunHandleV1, *, after_sequence: int = 0, limit: int = 1000,
page_token: str | None = None, deadline: datetime,
) -> RunEventPageV1: ...
def reconcile(
self, handle: RunHandleV1, *, now: datetime, deadline: datetime,
) -> ReconciliationReportV1: ...
Every public operation requires a UTC deadline. An expired deadline raises
DeadlineExceededError before the operation changes controller state.
submit hashes, rather than retains, the idempotency key. Reusing that key
with the same specification returns the original handle; reusing it with a
different specification raises IdempotencyConflictError. A handle produced
by a different controller raises RunHandleError.
pause, resume, and cancel are compare-and-swap operations: callers
provide the state they observed, and a stale or disallowed transition raises
RunConflictError. A production provider adapter is responsible for driving
the transitional PAUSING and CANCELLING states to their terminal
counterparts. The reference-only reference_progress and
reference_unit_* helpers exist to exercise that provider boundary in tests;
they are not an execution backend API.
events creates a 24-hour, per-controller snapshot on the first request.
Continuation tokens are bound to the handle, snapshot, limit, issuance and
expiry times, canonical payload digest, and a controller-local secret. A
foreign, malformed, modified, missing, or expired token raises
PaginationTokenError with a machine-readable code. New events never alter
an existing snapshot.
reconcile compares the reference unit state with explicit provider
observations. It can adopt a provider success, requeue a missing running
unit, mark a failed unit, or cancel an orphan. Orphan cancellation crosses an
explicit callback boundary configured with set_reference_orphan_canceller;
without that callback the operation fails with RunConflictError rather than
claiming that a provider cancellation occurred. Reconciliation appends a
non-lifecycle-mutating event and returns an immutable report.
Lifecycle and unit rules¶
The permitted lifecycle edges are PENDING to RUNNING or CANCELLING,
RUNNING to PAUSING, SUCCEEDED, FAILED, or CANCELLING, PAUSING to
PAUSED, FAILED, or CANCELLING, PAUSED to RUNNING or CANCELLING,
and CANCELLING to CANCELLED or FAILED. Terminal states cannot
transition. A run cannot become SUCCEEDED until every unit has succeeded.
The reference helpers start a unit only while the run is RUNNING, enforce
the configured in-flight limit, and require consecutive attempts. A retry is
allowed only after a retryable failure whose code is listed in
RetryPolicyV1.retryable_codes, and never beyond max_attempts. A unit can
be committed once for an attempt; its optional billing ID is retained in the
unit event. These helpers model control-protocol evidence, not Spark task
execution.
Class index¶
Class |
Kind |
Notes |
|---|---|---|
abc |
ABC; subclass to add a custom backend. Documented in the Abstract base classes section below |
|
concrete |
BYO Spark cluster; takes a |
|
concrete |
Synchronous one-host execution. Default for tests, CI, and small runs. |
|
convenience class |
Synchronous single-process API for getting-started and testing; not a |
|
concrete |
GCP Dataproc Serverless for Spark. Ships in |
|
concrete |
AWS EMR-on-EC2 / EMR Serverless. Ships in |
|
concrete |
Azure Synapse Spark pools. Ships in |
|
concrete |
Multi-cloud Databricks Jobs. Ships in |
|
dataclass |
One unit of work passed to |
|
dataclass |
Serializable sample-level or shard-level failure record |
|
dataclass |
Terminal per-shard worker summary returned by |
|
dataclass |
Opaque handle returned by |
|
dataclass |
Terminal outcome returned by |
|
dataclass |
Deterministic shard/sample seed planner accepted by |
|
|
dataclass |
Advanced testing/alternate-runtime helper for |
factory function |
Produces the per-shard worker callable passed to |
For per-cloud setup (service accounts, network, quotas) see Reference / Compute Environment. For cluster selection at runtime see Reference / Cloud Backends § Executor configuration.
ABC: BaseDistributedExecutor¶
The executor ABC every backend implements. Defines submit, wait,
cancel, and schema. It is part of the executor layer, not of
rfgen.orchestration; rfgen.orchestration owns the shard worker
factory and orchestration-side value types such as
ShardSpec,
RunResult, and
SeedSchedule. This page is
the normative reference for the ABC contract and its adjacent executor
surfaces.
class rfgen.executors.SparkExecutor¶
class SparkExecutor(BaseDistributedExecutor):
"""BYO Spark distributed executor. Maps shards onto Spark tasks using a
user-supplied `SparkSession` factory; runs against any Spark cluster
(local, standalone, YARN, Kubernetes) without depending on a vendor SDK.
Cloud-managed variants (`ManagedSparkServerlessExecutor`, `EMRExecutor`,
`SynapseExecutor`, `DatabricksExecutor`) subclass this class and override
only the `SparkSession` construction and credential plumbing.
"""
name: str = "spark"
Constructor¶
SparkExecutor(
*,
app_name: str = "rfgen",
session_factory: Callable[[], "SparkSession"] | None = None,
max_executors: int = 8,
executor_cores: int = 4,
executor_memory: str = "8g",
parallelism_per_executor: int = 1,
shard_partitioning: Literal["one_per_task", "coalesce", "repartition"] = "one_per_task",
fail_fast: bool = False,
poll_interval_s: float = 5.0,
credentials: BaseCredentialsProvider | None = None,
)
Stateless aside from configuration. The SparkSession is created lazily on
first submit; wait and cancel reuse the session held by the
ExecutorRunHandle.
Class attributes¶
Attribute |
Type |
Value |
Purpose |
|---|---|---|---|
|
str |
|
Registry key under the |
Constructor parameters¶
Name |
Type |
Default |
Description |
|---|---|---|---|
|
str |
|
|
|
callable |
|
Zero-arg factory returning a configured |
|
int |
|
|
|
int |
|
|
|
str |
|
|
|
int |
|
Concurrent shards per executor JVM. Above 1 only safe when emitters/channels are GIL-free |
|
str |
|
Strategy mapping shards to Spark partitions. |
|
bool |
|
If |
|
float |
|
How often |
|
|
Resolves storage credentials for shard outputs. |
Method: submit¶
def submit(
self,
shard_fn: Callable[[ShardSpec], ShardResult],
shards: list[ShardSpec],
) -> ExecutorRunHandle
Dispatch shards to the Spark cluster, one task per ShardSpec. shard_fn
must be picklable (Spark serializes it to workers); the rfgen runtime ships
a closure that loads the config, instantiates emitter / channel / store, and
writes one shard’s worth of samples.
Parameters¶
Name |
Type |
Required |
Default |
Description |
|---|---|---|---|---|
|
|
yes |
– |
Per-shard worker function. Writes to |
|
|
yes |
– |
Shard specs. Length determines the RDD partition count under |
Returns¶
An ExecutorRunHandle with
backend_name="spark" and backend_id set to the Spark application ID
(spark.app.id). Pass this to wait or cancel.
Raises¶
ConfigurationError if
session_factoryisNoneand the builder cannot resolve a master URL from environment / Spark conf.RuntimeErrorifshard_fnis not picklable; surfaces the underlyingpickle.PicklingErroras the cause.
Example¶
from rfgen.executors import SparkExecutor, ShardSpec
from rfgen.orchestration import build_shard_fn
executor = SparkExecutor(app_name="rfgen-demo", max_executors=16)
shards = [
ShardSpec.for_shard(
shard_index=i,
num_samples=4096,
config_hash="sha256:abc...",
output_uri="gs://rf-fm-datasets-synth/demo/v1",
)
for i in range(64)
]
handle = executor.submit(build_shard_fn(cfg), shards)
result = executor.wait(handle)
assert not result.failed_shards
Method: wait¶
def wait(
self,
handle: ExecutorRunHandle,
timeout: float | None = None,
) -> RunResult
Block until every Spark task associated with handle reaches a terminal
state, or until timeout seconds elapse. Polls Spark’s REST API at
poll_interval_s; aggregates per-task outcomes into RunResult. Worker callables return serializable ShardResult summaries; executors classify a shard as failed from that returned summary (for example, when ShardResult.error_records carries a terminal ShardFailureError) instead of depending on the worker to raise out of the task.
Returns¶
A RunResult. succeeded_shards lists
shard IDs that wrote successfully; failed_shards lists shard IDs that
ended in failure; errors carries the flattened ShardError records from those
shards; elapsed_s is wall-clock time from submit to terminal.
Raises¶
TimeoutErroriftimeoutelapses before all tasks terminate. The handle remains valid; pass it tocancelor callwaitagain.
Method: cancel¶
def cancel(self, handle: ExecutorRunHandle) -> None
Abort all in-flight tasks for handle via SparkContext.cancelJobGroup.
Idempotent: cancelling an already-terminal run is a no-op.
Method: schema¶
def schema(self) -> type[BaseModel]
Returns the Pydantic v2 model used by the config validator for the executor:
config block. Field names mirror the constructor parameters.
Schema: SparkExecutorConfig¶
class SparkExecutorConfig(BaseModel):
name: Literal["spark"] = "spark"
app_name: str = "rfgen"
max_executors: int = 8
executor_cores: int = 4
executor_memory: str = "8g"
parallelism_per_executor: int = 1
shard_partitioning: Literal["one_per_task", "coalesce", "repartition"] = "one_per_task"
fail_fast: bool = False
poll_interval_s: float = 5.0
Returned by SparkExecutor.schema() and consumed by the Hydra config layer.
Notes¶
No vendor SDK. SparkExecutor imports only
pyspark; cloud-managed variants subclass it and lazy-import their vendor SDK in__init__.Idempotency. Worker re-runs of a shard write the same
sample_idpaths (content-hashed); Spark’s speculative execution and task retries are safe. See the idempotency note on ZarrStore.Credentials. When
credentialsisNonethe cluster’s ambient identity is used (Dataproc service account, EMR instance profile, Databricks passthrough). For BYO Spark, pass an explicit BaseCredentialsProvider.Determinism. Each
ShardSpec.config_hashplusshard_indexseeds the per-shard RNG insideshard_fn; the executor itself contributes no randomness.
See Also¶
API Reference / BaseDistributedExecutor - ABC contract
Reference / Cloud Backends - extras and registered names
Reference / Compute Environment - per-cloud setup
ShardSpec, ExecutorRunHandle, RunResult - adjacent dataclasses
class rfgen.executors.LocalExecutor¶
class LocalExecutor(BaseDistributedExecutor):
"""Synchronous executor for tests, CI smoke runs, and small
single-host generation."""
name: str = "local"
submit runs every incomplete shard before returning its terminal handle;
wait aggregates those retained results. ExecutorConfig.parallelism is
ignored by local execution and controls the requested partition count only for
the live PySpark executor. Importing LocalExecutor
pulls no third-party dependency.
class rfgen.orchestration.local.LocalRunner¶
class LocalRunner:
"""Synchronous single-process convenience runner.
Runs Phase 1 IQ generation in-process, returning Records directly.
Not a BaseDistributedExecutor; no shards, no submit/wait. Intended for
getting-started scripts, integration tests, and notebook exploration.
For production multi-host generation use SparkExecutor.
Lives in src/rfgen/orchestration/local.py alongside LocalExecutor.
"""
Kind. Convenience class. NOT a subclass of BaseDistributedExecutor.
Module. rfgen.orchestration.local (src/rfgen/orchestration/local.py).
Constructor¶
LocalRunner(cfg: GenerationConfig)
Name |
Type |
Description |
|---|---|---|
|
Fully validated generation config. Use LocalRunner.build_config to construct one from a preset, or pass a manually validated config directly. |
Class method: build_config¶
@classmethod
def build_config(
cls,
preset: str,
overrides: dict[str, object] | None = None,
) -> GenerationConfig
Build a validated GenerationConfig from a named preset and an optional dict of Hydra-dotted-path overrides.
Parameters¶
Name |
Type |
Description |
|---|---|---|
|
str |
Name of a shipped preset (see Scenario Presets), e.g. |
|
|
Hydra dotted-path overrides applied on top of the preset defaults, e.g. |
Returns¶
A fully merged, validated GenerationConfig. Raises pydantic.ValidationError if the merged result is invalid.
Method: run¶
def run(self) -> Iterator[Record]
Generate all samples defined by cfg.run.num_samples synchronously in the calling process. Yields one Record per scene.
Returns¶
An Iterator[Record]. Callers can collect all records with list(runner.run()) or iterate lazily to bound memory. The generator respects cfg.storage.path only for manifest writing; the Record objects yielded in-process are fully assembled (IQ + labels + metadata). IQ is not written to disk unless cfg.storage.write_mode is set.
Raises¶
pydantic.ValidationErrorif config constraints are violated at runtime.SceneError if the scene composer receives an invalid runtime contract, such as a geometry mismatch or a time-placement strategy that returns no start samples for a sampled emitter.
Example¶
from rfgen.orchestration.local import LocalRunner
cfg = LocalRunner.build_config(
preset="narrowband_classifier_baseline",
overrides={"run.num_samples": 5, "storage.path": "./out/demo"},
)
runner = LocalRunner(cfg)
records = list(runner.run()) # returns list[Record]
record = records[0]
print(record.iq.shape) # torch.Size([2, 1_000_000])
print(record.scene.scene_geometry_hash)
print(len(record.bboxes)) # number of bounding boxes
Notes¶
Not sharded.
LocalRunnerruns allnum_samplesrecords sequentially in the calling process. For parallel execution use SparkExecutor with build_shard_fn.RNG is deterministic. The run uses
cfg.run.seed; the same config and seed reproduce the same records.Phase 2 not included.
run()is Phase 1 only. To annotate the generated records, run the annotation pipeline separately against the store path.
class rfgen.executors.ManagedSparkServerlessExecutor¶
class ManagedSparkServerlessExecutor(SparkExecutor):
"""GCP Dataproc Serverless for Spark. Subclasses `SparkExecutor`;
constructs the `SparkSession` against a Dataproc Serverless batch and
plugs in `GoogleADCProvider` credentials."""
name: str = "gcp_serverless_spark"
Constructor adds project_id, region, service_account, machine_type,
network_uri, subnetwork_uri. Ships in rfgen[gcp]; lazy-imports
google.cloud.dataproc_v1 on first construction. (stub.)
class rfgen.executors.EMRExecutor¶
class EMRExecutor(SparkExecutor):
"""AWS EMR-on-EC2 (cluster_id) or EMR Serverless (application_id).
Subclasses `SparkExecutor`; submits step-runs against an existing EMR
cluster using `boto3`."""
name: str = "emr"
Constructor adds cluster_id or application_id, release_label,
instance_type, instance_role_arn, log_uri. Ships in rfgen[aws];
lazy-imports boto3. (stub.)
class rfgen.executors.SynapseExecutor¶
class SynapseExecutor(SparkExecutor):
"""Azure Synapse Spark pool executor. Subclasses `SparkExecutor`;
submits Spark batch jobs against a named Synapse pool."""
name: str = "synapse"
Constructor adds workspace_name, spark_pool_name, tenant_id,
subscription_id, resource_group. Ships in rfgen[azure]; lazy-imports
azure.synapse.spark. (stub.)
class rfgen.executors.DatabricksExecutor¶
class DatabricksExecutor(SparkExecutor):
"""Multi-cloud Databricks executor. Subclasses `SparkExecutor`; submits
one-time Jobs Runs against an existing Databricks workspace and reuses
its job clusters."""
name: str = "databricks"
Constructor adds host, token_env, cluster_id or job_cluster_spec,
notebook_path or python_file_uri. Ships in rfgen[databricks];
lazy-imports databricks.sdk. Works on AWS, Azure, and GCP Databricks
deployments. (stub.)
class rfgen.executors.ShardSpec¶
@dataclass(frozen=True)
class ShardSpec:
shard_id: str
shard_index: int
num_samples: int
config_hash: str
output_uri: str
One unit of work passed into submit. Frozen and hash-stable so the same
spec can be retried byte-identically. shard_id must be the canonical
content-addressed id for (config_hash, shard_index); use
ShardSpec.for_shard(...) to derive it.
Field |
Type |
Description |
|---|---|---|
|
str |
Canonical content-addressed shard identifier, derived from |
|
int |
Zero-based index. Combined with |
|
int |
Number of Record records this shard must produce |
|
str |
|
|
str |
Where the shard’s records land. URL scheme picks the storage filesystem ( |
Method: canonical_shard_id¶
@classmethod
def canonical_shard_id(
cls,
*,
config_hash: str,
shard_index: int,
) -> str: ...
Returns "sha256:<lowercase hex digest>" for the canonical CBOR encoding of
{"config_hash": config_hash, "shard_index": shard_index}. The same two
inputs always return the same ID; changing either input changes the hash input.
Parameter |
Type |
Contract |
|---|---|---|
|
str |
Required non-empty string. Leading and trailing whitespace is stripped before hashing. The method does not parse or verify a |
|
int |
Zero-based integer greater than or equal to |
Returns str. Invalid parameters raise ConfigError naming ShardSpec.config_hash or
ShardSpec.shard_index.
Method: for_shard¶
@classmethod
def for_shard(
cls,
*,
shard_index: int,
num_samples: int,
config_hash: str,
output_uri: str,
) -> ShardSpec: ...
Calls canonical_shard_id(…) first, then constructs and returns a
fully validated ShardSpec with the resulting value in shard_id.
Parameter |
Type |
Contract |
|---|---|---|
|
int |
Zero-based integer greater than or equal to |
|
int |
Number of records requested from this shard; must be at least |
|
str |
Required non-empty string used by |
|
str |
Required non-empty destination URI or path; trimmed during validation. |
Returns ShardSpec. Invalid fields raise ConfigError naming the corresponding ShardSpec field.
Direct ShardSpec(...) construction additionally raises ConfigError when a
caller-supplied shard_id does not equal the value returned by
canonical_shard_id(...) for the same config_hash and shard_index.
Mirrored from the ABC definition; re-exported here for convenience.
class rfgen.orchestration.ShardError¶
@dataclass(frozen=True)
class ShardError:
sample_idx: int
error_class: str
message: str
traceback: str
Immutable failure record carried by ShardResult and flattened into RunResult. It stores strings and integers only, so a distributed executor can pickle or otherwise serialize it without preserving the original exception object.
Field |
Type |
Valid values |
Description |
|---|---|---|---|
|
int |
|
Zero-based failed sample index. |
|
str |
Non-empty after trimming |
Exception class name, such as |
|
str |
Non-empty after trimming |
Human-readable exception message. |
|
str |
Non-empty after trimming |
Captured traceback text. Synthetic shard-level errors use a one-line |
Construction trims all three string fields. Invalid field types, a
sample_idx below -1, bool/float/string substitutes for the integer index,
or an empty string raise ConfigError naming the invalid field.
class rfgen.orchestration.ShardResult¶
@dataclass(frozen=True)
class ShardResult:
shard_id: str
succeeded_samples: int
failed_samples: int
elapsed_s: float
error_records: tuple[ShardError, ...] = ()
Terminal summary returned by one shard worker. Executors aggregate these per-shard outcomes into RunResult.
A shard has two separately stored parts: its records and a small shard-complete flag. The flag is the authoritative statement that every requested record finished. A failed attempt can therefore leave usable records behind without the flag; the next run sees the missing flag and tries the shard again.
Here, commit means publishing that flag after all records finish. It does not promise database-style rollback of records written earlier in the attempt.
Each record has an ID derived from its content, so a deterministic retry regenerates the same ID. Writing identical content under an ID that already exists is an idempotent replay: the store treats it as a no-op rather than creating a duplicate.
Field |
Type |
Description |
|---|---|---|
|
str |
Canonical shard identifier matching the submitted ShardSpec |
|
int |
Count of records that completed scene composition and labeling during this attempt. A completed-shard skip reports |
|
int |
Count of caught sample-local composition or labeling failures. A shard-level storage, channel, or RT asset-preparation failure reports |
|
float |
Worker wall-clock seconds for this shard attempt |
|
|
Terminal shard-level and sample-level failures captured without raising out of the worker |
For a normal attempt, the worker increments succeeded_samples immediately
before yielding each labeled record to
StoreHandle.write_shard(…). The
count therefore describes generated and labeled records, not records guaranteed
to remain stored when a later sample failure aborts the shard commit. A backend
that stages the entire shard may discard every yielded record, while a backend
that writes records individually may retain them without publishing the
shard-complete flag.
With RunConfig.fail_fast = False, sample iteration attempts every requested
sample, so succeeded_samples + failed_samples == ShardSpec.num_samples. With
fail_fast = True, iteration stops after the first sample-local failure, so
succeeded_samples + failed_samples <= ShardSpec.num_samples; a strict
inequality means the remaining samples were not attempted. Shard-level storage,
channel, and RT asset-preparation failures normalize the result to
succeeded_samples = 0 and
failed_samples = ShardSpec.num_samples.
Defined in rfgen.orchestration and consumed by
BaseDistributedExecutor.submit()
implementations through the Callable[[ShardSpec], ShardResult] worker
contract documented on this page.
class rfgen.executors.ExecutorRunHandle¶
@dataclass(frozen=True)
class ExecutorRunHandle:
backend_name: str
backend_id: str
Opaque handle returned by submit and consumed by wait / cancel.
Field |
Type |
Description |
|---|---|---|
|
str |
Matches the executor’s |
|
str |
Vendor-native Spark application or managed-service job identifier. |
Mirrored from the ABC definition.
class rfgen.executors.RunResult¶
@dataclass(frozen=True)
class RunResult:
succeeded_shards: tuple[str, ...]
failed_shards: tuple[str, ...]
elapsed_s: float
errors: tuple[ShardError, ...]
Terminal outcome returned by wait.
Field |
Type |
Description |
|---|---|---|
|
|
Shard IDs whose |
|
|
Shard IDs whose |
|
float |
Wall-clock seconds from |
|
|
Flattened shard-level and sample-level error records, in arrival order across the run |
Mirrored from the ABC definition.
Abstract base classes¶
BaseDistributedExecutor¶
Phase 1 sharded IQ generation. Execution is synchronous locally or distributed through PySpark and its managed-service variants.
from dataclasses import dataclass
@dataclass(frozen=True)
class ShardSpec:
shard_id: str
shard_index: int
num_samples: int
config_hash: str
output_uri: str
@dataclass(frozen=True)
class ExecutorRunHandle:
backend_name: str
backend_id: str # e.g. Spark or managed-service batch ID
@dataclass(frozen=True)
class RunResult:
succeeded_shards: tuple[str, ...]
failed_shards: tuple[str, ...]
elapsed_s: float
errors: tuple[ShardError, ...]
class BaseDistributedExecutor(ABC):
"""Phase 1 sharded IQ generation orchestrator."""
name: str # registry key: "local", "spark", "gcp_serverless_spark", ...
@abstractmethod
def submit(
self,
shard_fn: Callable[[ShardSpec], ShardResult],
shards: list[ShardSpec],
) -> ExecutorRunHandle: ...
@abstractmethod
def wait(
self,
handle: ExecutorRunHandle,
timeout: float | None = None,
) -> RunResult: ...
@abstractmethod
def cancel(self, handle: ExecutorRunHandle) -> None: ...
@abstractmethod
def schema(self) -> type[BaseModel]:
"""Pydantic config model accepted by this executor's constructor."""
submit()¶
Abstract method on BaseDistributedExecutor. Dispatch a list of ShardSpec to the cluster, returning an ExecutorRunHandle.
wait()¶
Abstract method on BaseDistributedExecutor. Block until every shard reaches a terminal state (or timeout elapses); returns a RunResult.
cancel()¶
Abstract method on BaseDistributedExecutor. Abort all in-flight tasks for the supplied handle. Idempotent.
schema()¶
Abstract method on BaseDistributedExecutor. Returns the Pydantic config model accepted by this executor.
Implementations:
Subclass |
Cloud |
Extra |
|---|---|---|
none |
core |
|
none (any Spark cluster) |
core |
SparkExecutor plans shard specifications in 10,000-item chunks, builds one
flat SparkContext.union over those chunks, and retains only the configured
result-detail bound in its handle. The acceptance suite exercises this path
with 100,000 lazily generated shard specifications.
| ManagedSparkServerlessExecutor | GCP | rfgen[gcp] |
| EMRExecutor | AWS | rfgen[aws] |
| SynapseExecutor | Azure | rfgen[azure] |
| DatabricksExecutor | multi-cloud | rfgen[databricks] |
class rfgen.rng.SeedSchedule¶
from rfgen.rng import SeedSchedule
class SeedSchedule:
global_seed: int
num_shards: int
samples_per_shard: int
@classmethod
def plan(
cls,
global_seed: int,
num_shards: int,
samples_per_shard: int,
) -> SeedSchedule: ...
def sample_seed(
self,
shard_index: int,
sample_idx: int,
*,
stage: str | int = 0,
emitter_idx: int = 0,
rx_idx: int = 0,
) -> int: ...
def shard_sample_seed(
self,
shard_id: str,
sample_idx: int,
*,
stage: str | int = 0,
emitter_idx: int = 0,
rx_idx: int = 0,
) -> int: ...
Module. rfgen.rng (src/rfgen/rng.py). Re-exported from
rfgen.orchestration for backward compatibility with existing executor and
test code.
Deterministic seed planner for Phase 1 shard execution. Executors rarely
construct one directly; the main user-facing seam is the optional
seed_schedule= override on
build_shard_fn.
Constructor surface¶
Prefer the validated class method:
schedule = SeedSchedule.plan(
global_seed=1337,
num_shards=64,
samples_per_shard=4096,
)
SeedSchedule.plan(...) validates that all three dimensions are real
integers and that num_shards > 0 and samples_per_shard > 0.
Fields¶
Name |
Type |
Description |
|---|---|---|
|
|
Run-level root seed threaded into seed_for |
|
|
Finite shard count for this run |
|
|
Planned samples per shard, used for index validation and the default per-shard memoization path |
Methods¶
Method |
Returns |
Description |
|---|---|---|
|
Validated constructor used by orchestration and tests |
|
|
|
Index-based seed lookup for a synthetic shard id ( |
|
|
Seed lookup bound to an explicit canonical shard id string |
Notes¶
The default worker path memoizes each shard’s
(emitter_idx=0, rx_idx=0, stage=0)per-sample seeds on first use so repeatedsample_idxlookups do not rebuild a fresh NumPySeedSequencechain every time.The default worker path keeps only the most recently used shard’s
(emitter_idx=0, rx_idx=0, stage=0)per-sample seeds memoized at a time, so the hot loop stays amortized without letting long-lived workers retain one full seed tuple per historical shard.The memoized cache is worker-local runtime state. Pickle round-trips clear it so distributed executors do not ship warmed per-shard seed tuples between processes.
build_shard_fn(...)validates any explicit schedule override against the run-derived shard geometry before returning the worker callable.
Advanced Testing Helper: ShardWorkerCollaborators¶
from rfgen.orchestration import ShardWorkerCollaborators
from rfgen.emitters.base import EmitterLike
@dataclass(frozen=True)
class ShardWorkerCollaborators:
store_factory: Callable[[GenerationConfig], BaseStore] | None = None
channel_factory: Callable[[GenerationConfig], BaseChannel | ChannelPipeline] | None = None
composer_factory: Callable[
[GenerationConfig, BaseChannel | ChannelPipeline],
BaseSceneComposer,
] | None = None
labeler_factory: Callable[[GenerationConfig], BaseLabeler] | None = None
emitter_pool_factory: Callable[[GenerationConfig], Mapping[str, EmitterLike]] | None = None
Optional override bundle for tests and alternate runtimes that need a custom
worker-local store, channel, composer, labeler, or emitter pool. The default
build_shard_fn(cfg) call shape stays narrow; day-to-day callers should ignore
this helper even though it is exported by
from rfgen.orchestration import *.
Each factory is optional. When omitted, build_shard_fn(...) uses the normal
framework path for that collaborator. When provided, the factory must be
pickleable and have the runtime call shape that orchestration uses:
store_factory(cfg)returning a BaseStorechannel_factory(cfg)returning a BaseChannel or ChannelPipelinecomposer_factory(cfg, channel)returning a BaseSceneComposer; itsbuild(...)method accepts the validated scene config, emitter pool, channel, and RNG, and returns a Signallabeler_factory(cfg)returning a BaseLabeleremitter_pool_factory(cfg)returningMapping[str, EmitterLike]
Advanced alternate-device runtimes can also implement the public structural protocol below on either the resolved channel or composer:
from typing import Protocol, runtime_checkable
from rfgen.orchestration import SupportsSampleRngDevice
@runtime_checkable
class SupportsSampleRngDevice(Protocol):
def sample_rng_device(self) -> torch.device | str | None: ...
Orchestration checks the composer first. If the composer hook returns a valid
non-None device, that device wins and the channel hook is not consulted. The
channel hook is used only when the composer omits the hook or returns None.
Without either explicit hook, orchestration keeps the sample RNG on CPU.
build_shard_fn(...) validates the factory signatures at construction time.
The worker validates collaborator method signatures, the channel return type,
and the opened store-handle protocol before it runs any shard records.
build_shard_fn¶
from rfgen.orchestration import build_shard_fn
def build_shard_fn(
cfg: GenerationConfig,
*,
seed_schedule: SeedSchedule | None = None,
collaborators: ShardWorkerCollaborators | None = None,
) -> Callable[[ShardSpec], ShardResult]: ...
Module. rfgen.orchestration (defined in src/rfgen/orchestration.py).
Factory that produces the per-shard worker callable passed to BaseDistributedExecutor.submit(). The returned callable is self-contained: it holds a closed-over copy of the resolved config and carries everything a worker process needs to generate one shard without any other context.
Parameters¶
Name |
Type |
Description |
|---|---|---|
|
Fully resolved and validated generation config. Must be fully merged (all Hydra defaults applied, all validators passed) before this call. The factory snapshots it with |
|
|
SeedSchedule | |
Optional explicit shard seed schedule. When omitted, the factory derives one from |
|
|
Advanced testing/alternate-runtime escape hatch. When omitted, |
Returns¶
A Callable[[ShardSpec], ShardResult] with the following contract:
Input: one ShardSpec.
Output: one ShardResult. Records are still written to
shard.output_urias a side effect.Picklable: the returned callable must survive
pickle.dumps/pickle.loads, which Spark uses to ship it to executor JVMs. The snapshotted Pydantic config and the seed schedule are carried directly inside the worker object. Any collaborator factory insideShardWorkerCollaboratorsmust itself be pickleable. Worker-local runtime state such as prepared RT assets, collaborator instances, and the asset cache root is recomputed after unpickling, so deserialized workers do not reuse the sender process’s local temp paths.Validated overrides: collaborator-factory signatures and pickleability are checked at
build_shard_fn(...)construction time. On every shard, the worker validates the store’s bounddataset_uri()contract before probinghas_shard(...), so a divergentShardSpec.output_urifails even on a completed-shard rerun. The completed-shard fast path still avoids instantiating heavy non-store collaborators, so actual non-store collaborator-object validation happens only on an incomplete shard, where the worker validates collaborator method signatures, sample-RNG-device hooks, and the opened store-handle protocol before any shard records run. Invalid overrides fail withConfigErrornaming the offending collaborator field.Bound dataset URI wins: when the resolved store exposes
dataset_uri(),ShardSpec.output_urimust match that bound dataset URI exactly before the shard-complete probe runs. Orchestration then opensStoreMode.APPENDon the bound URI, so the shard-complete probe and the append target cannot drift to different datasets.
What the returned callable does¶
def _shard_fn(spec: ShardSpec) -> ShardResult:
# 1. Read the already-validated config snapshot from closure
cfg = snapshotted_cfg
schedule = seed_schedule or SeedSchedule.plan(
global_seed=cfg.run.seed,
num_shards=ceil(cfg.run.num_samples / cfg.run.shard_size),
samples_per_shard=cfg.run.shard_size,
)
# 2. Resolve the store, validate the bound dataset URI, then probe the shard-complete marker
store = cached_store or build_store(cfg.storage)
append_uri = cached_bound_dataset_uri or store.dataset_uri() or spec.output_uri
if append_uri != spec.output_uri:
raise ConfigError("ShardSpec.output_uri must match the bound store dataset URI.")
if store.has_shard(spec.shard_id):
return ShardResult(
shard_id=spec.shard_id,
succeeded_samples=spec.num_samples,
failed_samples=0,
elapsed_s=elapsed_s,
)
# 3. Resolve and validate any remaining collaborators only for incomplete shards
channel = cached_channel or build_channel_pipeline(cfg.channel)
composer = cached_composer or build_composer(cfg, channel=channel)
labeler = cached_labeler or build_labeler(cfg)
emitter_pool = cached_emitter_pool or build_emitter_pool(cfg)
validate_composer_signature(composer.build)
validate_labeler_signature(labeler.label)
validate_emitter_pool(emitter_pool)
sample_rng = cached_sample_rng or new_sample_rng(composer=composer, channel=channel)
succeeded = 0
errors = []
def records():
nonlocal succeeded
for sample_idx in range(spec.num_samples):
sample_rng.manual_seed(
schedule.shard_sample_seed(spec.shard_id, sample_idx)
)
try:
scene_signal = composer.build(
scene_cfg=cfg.scene,
emitter_pool=emitter_pool,
channel=channel,
rng=sample_rng,
)
labels = labeler.label(
iq=scene_signal.iq,
emitters=tuple(
component.metadata
for component in scene_signal.component_signals
),
scene=scene_signal.metadata,
)
except Exception as exc:
errors.append(ShardError.from_exception(spec, sample_idx, exc))
if cfg.run.fail_fast:
break
continue
succeeded += 1
yield labels
if errors:
# Internal control flow: stop write_shard before it can publish its
# backend-specific shard-complete marker.
raise AbortShardCommit
with store.open(append_uri, mode=StoreMode.APPEND) as handle:
try:
handle.write_shard(spec.shard_id, records())
except AbortShardCommit:
pass
failed = len(errors)
if failed / spec.num_samples >= cfg.run.shard_failure_threshold:
errors.append(shard_failure_error(spec.shard_id))
return ShardResult(
shard_id=spec.shard_id,
succeeded_samples=succeeded,
failed_samples=failed,
elapsed_s=elapsed_s,
error_records=tuple(errors),
)
AbortShardCommit, ShardError.from_exception(...), and
shard_failure_error(...) above are descriptive pseudocode. The implementation
uses private helpers for the same control flow and constructs the public frozen
result records explicitly. The labeler returns the complete Record; there is
no separate record-assembly call after labeling.
Notes¶
Streaming commit boundary. The worker passes a lazy record iterator to StoreHandle.write_shard(…). A backend may persist each yielded record immediately, but it must publish its shard-complete marker only after exhausting the iterator normally. If any sample fails, the iterator raises the private abort after its last successful record;
write_shard(...)therefore does not return and must not publish the marker. The worker catches only that private abort and still returns the accumulated sample results.Idempotency. The worker writes through the store’s content-addressed sample IDs. A shard retry replays the same successful records to the same
sample_idvalues, and store-owned skip-if-exists handling makes those sample writes a no-op. Because the failed attempt has no shard-complete marker,has_shard(...)remains false and the retry enters the generation path instead of taking the completed-shard fast path.No Phase 2. The callable is Phase 1 only: it does not call any inference backend or annotator. Phase 2 is a separate job that appends to the store after Phase 1 completes.
Error handling. Exceptions from scene composition or labeling are sample-local: each becomes one
ShardError, and generation continues unlesscfg.run.fail_fastis true. Any sample-local failure aborts the storage shard commit before a shard-complete marker is published. Storage or RT asset-preparation failures are returned as shard-level error records withsample_idx = -1. After sample iteration stops, the worker computesfailed_samples / spec.num_samples; the denominator is always the requested shard size, including when fail-fast leaves later samples unattempted. A ratio greater than or equal toshard_failure_thresholdappends a terminalShardFailureErrorwithsample_idx = -1. Threshold breaches stay inside the returnedShardResultso distributed executors can serialize, aggregate, and classify the shard without relying on task-level exceptions.
Example¶
from rfgen.config import GenerationConfig
from rfgen.executors import SparkExecutor, ShardSpec
from rfgen.orchestration import build_shard_fn
from omegaconf import OmegaConf
raw = OmegaConf.to_container(OmegaConf.load("configs/config.yaml"), resolve=True)
cfg = GenerationConfig.model_validate(raw)
shard_fn = build_shard_fn(cfg)
cfg_hash = "sha256:cfg"
# Verify picklability before submitting to Spark
import pickle
pickle.loads(pickle.dumps(shard_fn)) # must not raise
executor = SparkExecutor(app_name="rfgen-demo", max_executors=16)
shards = [
ShardSpec.for_shard(
shard_index=i, num_samples=4096,
config_hash=cfg_hash,
output_uri="gs://my-bucket/dataset/v1",
)
for i in range(64)
]
handle = executor.submit(shard_fn, shards)
result = executor.wait(handle)
assert not result.failed_shards
See Also¶
Concepts / Architecture - where the executor sits in the pipeline
Reference / Cloud Backends - extras matrix and registered names
Reference / Compute Environment - per-cloud setup recipes
Reference / API / Executors § BaseDistributedExecutor - normative ABC contract
Reference / Plugin Metadata - third-party executor packaging
Reference / API / Storage - where shard outputs land