Run on Spark¶
Submit a sharded generation job to GCP Managed Spark serverless (formerly Dataproc Serverless).
Warning
Pre-implementation. Targets v0.5+.
When to use Spark¶
Scale |
Recommended runner |
|---|---|
< 10 K samples |
|
10 K – 1 M samples |
|
1 M – 100 M samples |
Spark serverless |
100 M+ samples |
Spark serverless with custom executor pools |
Spark gives you autoscaling, per-shard idempotency, GCS-native I/O, and integrated monitoring, at the cost of needing GCP infra.
Prerequisites¶
pip install "rfgen[spark]"
# GCP setup
gcloud auth application-default login
gcloud config set project $GCP_PROJECT
gcloud services enable dataproc.googleapis.com
You’ll need:
A GCS bucket for output (e.g.,
gs://rf-fm-datasets-synth)A service account with Dataproc Worker, Storage Admin, and Vertex AI User roles
Managed Spark serverless API enabled
The orchestration config¶
# configs/orchestration/spark_serverless.yaml
backend: spark_serverless
parallelism: 32 # requested PySpark partition count
spark:
project_id: ${oc.env:GCP_PROJECT}
region: us-central1
service_account: rfgen-runner@${oc.env:GCP_PROJECT}.iam.gserviceaccount.com
machine_type: n2-standard-8 # 8 vCPU, 32 GB RAM
max_executors: 32
image_uri: gcr.io/${oc.env:GCP_PROJECT}/rfgen-spark:0.5.0
The image must contain rfgen plus all extras you need (TorchSig, Sionna, etc.). The framework ships a Dockerfile (docker/Dockerfile.spark) and a make spark-image target that builds + pushes it.
Submit¶
rfgen generate \
+preset=wideband_detection_baseline \
storage.path=gs://rf-fm-datasets-synth/wideband-baseline/v0.5.0 \
orchestration=spark_serverless \
orchestration.spark.project_id=$GCP_PROJECT
Output:
Submitting Spark batch job: rfgen-wideband-20260601-1432
region: us-central1
template: managed-spark-rfgen-v0.5.0
executors: autoscale 8–32 × n2-standard-8
estimated wall-clock: 3.5–5.5 hours
Job ID: 1234abcd-...
Console: https://console.cloud.google.com/dataproc/batches/us-central1/1234abcd...
Streaming job logs ... (Ctrl-C to detach; job continues)
Per-shard seeding¶
shard_seed = hash(global_seed, shard_id)
Every emitter, channel, and labeler in a shard derives its RNG state from shard_seed. Two re-runs with the same global seed and the same shard ID produce byte-identical IQ.
Monitor¶
# Job state
gcloud dataproc batches describe 1234abcd-... --region us-central1
# Logs
gcloud dataproc batches logs 1234abcd-... --region us-central1 --tail
# rfgen-side progress
rfgen inspect gs://rf-fm-datasets-synth/wideband-baseline/v0.5.0 summary
# Sample count: 347_000 / 1_000_000 (34.7%)
# Shards complete: 347 / 1000
# ...
Resource sizing¶
Preset |
Recommended config |
|---|---|
|
|
|
Spark, |
|
Spark, |
|
Spark + GPU pool, |
Anything 10M+ |
Spark with custom executor pool config |
GPU executors (for Sionna RT)¶
Sionna RT has a CPU fallback, but production ray tracing should use GPUs. Configure a GPU executor pool:
orchestration:
spark:
executor_pool:
cpu:
machine_type: n2-standard-8
max_executors: 32
gpu:
machine_type: g2-standard-8 # 1 × NVIDIA L4
max_executors: 8
accelerator: nvidia-l4
scene:
channel:
name: sionna_rt # framework routes to GPU pool automatically
The framework partitions work: RT-channel shards go to the GPU pool, comms-only shards stay on CPU. Mix is set by the per-emitter weights and the channel choice.
Cost¶
Rough order-of-magnitude (us-central1, 2026 prices):
Preset / size |
CPU-hours |
GPU-hours |
Cost |
|---|---|---|---|
|
~30 |
0 |
~$15 |
|
~280 |
0 |
~$140 |
|
~280 |
~120 |
~$420 |
Spot VMs cut this 60–80%; the framework defaults to spot with on-demand fallback.
Why Spark serverless and not GKE / Cloud Run / batch?
Per-job billing. No idle cluster. Submit, run, billed for the runtime, that’s it.
Autoscale. Scales executors based on partition count.
PySpark API. Embarrassingly-parallel shard generation maps cleanly onto
parallelize().map(...).GCS native. Reads/writes Zarr and WebDataset shards directly from / to GCS without intermediate copies.
No cluster ops. No nodes to patch, no autoscaler tuning, no idle cost.
Cloud Run scales but doesn’t fit the long-tasks-with-shuffle pattern. GKE Batch is fine but adds cluster overhead. For embarrassingly-parallel work at this scale, Managed Spark serverless wins.
Cancellation
Detaching from log streaming (Ctrl-C) does not cancel the job. To cancel:
gcloud dataproc batches cancel 1234abcd-... --region us-central1
Already-completed shards remain on GCS. Re-submitting picks up where the cancellation left off.
Provider-neutral managed-Spark control (Layer 19)¶
The shipped rfgen.managed_spark module is a strict control-plane contract,
not a Dataproc submission client. Production deployments provide a
provider-specific ManagedSparkAdapter; this release supplies only the
deterministic in-memory reference adapter for contract tests. Do not infer from
this page that the example CLI, provider maps, or cloud job submission are
implemented by Layer 19.
Construct ManagedSparkJobSpecV1 with a UTC deadline, wheel URI and SHA-256,
entry point, and immutable configuration maps. Keep credentials in the
provider identity/adapter boundary, not in request fields, arguments, or log
records. Reuse an idempotency key only for the identical request: an identical
retry returns the same handle, while a different request fails closed with an
idempotency conflict.
UNKNOWN is an ambiguous provider outcome. Reconcile it before wait or
cancel; if it remains unknown, stop and investigate rather than submit a
duplicate job. Cancellation has to converge within five minutes of its UTC
deadline. Log-page tokens are opaque and bound to their handle, so retain the
handle and returned token together rather than synthesizing an offset.
Local development without Spark
For dev/test today, orchestration=local stays single-process. Multi-process local execution is a future executor surface, not a shipped GenerationConfig.executor.parallelism behavior.
rfgen generate +preset=... orchestration=local