Curate TextSynthetic Data

NeMo Data Designer Integration

View as Markdown

NeMo Data Designer (NDD) is a declarative data generation framework that integrates with NeMo Curator to scale synthetic data pipelines. Instead of writing imperative large language model (LLM) call logic, you define the columns, structured-field sampling, and LLM. NDD handles execution, batching, and token metric collection automatically.

How It Works

NeMo Curator wraps NDD through the DataDesignerStage, which accepts a DataDesignerConfigBuilder or a YAML config file. The stage:

  1. Takes input records from a DocumentBatch.
  2. Passes them to NDD as a seed dataset.
  3. Calls DataDesigner.preview() to generate new columns for samplers, expressions, and LLM text.
  4. Returns the enriched dataset as a new DocumentBatch with token usage metrics.
API Calls Seed Data(JSONL/Parquet) DataDesignerStage NDD Engine(Samplers, Expressions,LLM Generation) Enriched Output(JSONL/Parquet) LLM Endpoint(Local InferenceServeror NVIDIA NIM)

Prerequisites

Install the NDD dependency:

uv pip install nemo-curator[sdg_cpu]

The data-designer package ships only in the synthetic data generation (SDG) extras: sdg_cpu and sdg_cuda12 for the GPU stack. It is not part of the text extras.

For local model serving on GPU, install sdg_cuda12, which pulls in cuda12, sdg_cpu, and inference_server together:

uv pip install \
--torch-backend cu129 \
--extra-index-url https://wheels.vllm.ai/0.22.0/cu129 \
"nemo-curator[sdg_cuda12]"

DataDesignerStage

The DataDesignerStage is the core integration point between NeMo Curator and NDD.

Parameters

The DataDesignerStage constructor accepts the following parameters:

ParameterTypeDefaultDescription
config_builderDataDesignerConfigBuilderNoneNDD configuration builder. Mutually exclusive with data_designer_config_file.
data_designer_config_filestrNonePath to a YAML configuration file. Mutually exclusive with config_builder.
model_providerslistNoneCustom ModelProvider instances for local or test endpoints. If the value is None, NDD uses its default providers.
verboseboolFalseShows complete NDD log output when set to True.

Metrics

DataDesignerStage automatically collects and reports:

  • ndd_running_time: Wall-clock time for the NDD preview() call
  • num_input_records / num_output_records: Record counts before and after generation
  • input_tokens_median_per_record / output_tokens_median_per_record: Median token counts across all LLM columns

Distributed Execution and Empty Batches

Ray can serialize and copy DataDesignerStage to workers even though the live Data Designer client contains process-local objects that are not serializable. The stage serializes its configuration builder and model providers, omits the live client, and recreates the client inside the destination worker. This process is done automatically via internal __getstate__, __setstate__, and _init_data_designer methods. Construct the stage normally, and do not create or replace its internal Data Designer client.

An upstream stage can legitimately produce an empty DocumentBatch. For example, a filter might remove every record in one input partition while other partitions still contain data. In that case, DataDesignerStage:

  1. Skips DataDesigner.preview(), so it makes no generation or judge requests for that batch.
  2. Returns an empty DocumentBatch while preserving its dataset name, metadata, and stage-performance history.
  3. Reports 0 for running time, input and output record counts, and median input and output tokens.

This behavior makes it safe to place filtering stages before Data Designer or between multiple Data Designer stages without special-casing fully filtered partitions.

Building a Configuration

NDD configurations use a builder pattern. You add columns of three types:

For complete guidance on building an NDD configuration, refer to the NDD config builder reference.

Sampler Columns

Generate structured data using built-in samplers (Faker names, UUIDs, dates):

import data_designer.config as dd
config_builder = dd.DataDesignerConfigBuilder(model_configs=[model_config])
config_builder.add_column(
dd.SamplerColumnConfig(
name="patient_name",
sampler_type=dd.SamplerType.PERSON_FROM_FAKER,
params=dd.PersonFromFakerSamplerParams(),
)
)
config_builder.add_column(
dd.SamplerColumnConfig(
name="patient_id",
sampler_type=dd.SamplerType.UUID,
params=dd.UUIDSamplerParams(prefix="PT-", short_form=True, uppercase=True),
)
)

Expression Columns

Derive values from other columns using Jinja templates:

config_builder.add_column(
dd.ExpressionColumnConfig(
name="first_name",
expr="{{ patient_name.first_name }}",
)
)

LLM Text Columns

Generate text using an LLM with prompts that reference other columns:

config_builder.add_column(
dd.LLMTextColumnConfig(
name="physician_notes",
prompt="""\
You are a primary-care physician who just had an appointment with {{ first_name }}.
{{ patient_summary }}
Write careful notes about your visit. Respond with only the notes.
""",
model_alias="local-llm",
)
)

End-to-End Example

This example generates synthetic medical notes from seed symptom data using a local InferenceServer:

import data_designer.config as dd
from nemo_curator.backends.ray_data import RayDataExecutor
from nemo_curator.core.client import RayClient
from nemo_curator.core.serve import InferenceServer, RayServeModelConfig
from nemo_curator.pipeline import Pipeline
from nemo_curator.stages.synthetic.nemo_data_designer.data_designer import DataDesignerStage
from nemo_curator.stages.text.io.reader.jsonl import JsonlReader
from nemo_curator.stages.text.io.writer.jsonl import JsonlWriter
# Start Ray cluster
client = RayClient(num_cpus=16, num_gpus=4)
client.start()
# Start local inference server
server_config = RayServeModelConfig(
model_identifier="google/gemma-3-27b-it",
deployment_config={"autoscaling_config": {"min_replicas": 1, "max_replicas": 1}},
engine_kwargs={"tensor_parallel_size": 4},
)
inference_server = InferenceServer(models=[server_config])
inference_server.start()
# Configure NDD model
model_config = dd.ModelConfig(
alias="local-llm",
model="google/gemma-3-27b-it",
provider="local",
skip_health_check=True,
inference_parameters=dd.ChatCompletionInferenceParams(
temperature=1.0, top_p=1.0, max_tokens=2048,
),
)
model_provider = dd.ModelProvider(
name="local",
endpoint=inference_server.endpoint,
api_key="unused",
)
# Build config with sampler and LLM columns
config_builder = dd.DataDesignerConfigBuilder(model_configs=[model_config])
config_builder.add_column(
dd.SamplerColumnConfig(
name="patient_name",
sampler_type=dd.SamplerType.PERSON_FROM_FAKER,
params=dd.PersonFromFakerSamplerParams(),
)
)
config_builder.add_column(
dd.LLMTextColumnConfig(
name="physician_notes",
prompt="You are a physician. Write notes for {{ patient_name.first_name }} "
"who has {{ diagnosis }}. {{ patient_summary }}",
model_alias="local-llm",
)
)
# Build and run pipeline
pipeline = Pipeline(name="ndd_medical_notes")
pipeline.add_stage(JsonlReader(file_paths="seed_data/*.jsonl", fields=["diagnosis", "patient_summary"]))
pipeline.add_stage(DataDesignerStage(config_builder=config_builder, model_providers=[model_provider]))
pipeline.add_stage(JsonlWriter(path="./synthetic_output"))
pipeline.run(executor=RayDataExecutor())
inference_server.stop()
client.stop()

Using a Remote Provider

To use NVIDIA NIM or another hosted endpoint instead of a local server, configure the ModelProvider with the remote URL and API key:

import os
import data_designer.config as dd
from nemo_curator.stages.synthetic.nemo_data_designer.data_designer import DataDesignerStage
model_config = dd.ModelConfig(
alias="nim-llm",
model="nvidia/nemotron-3-super-120b-a12b",
provider="nvidia",
inference_parameters=dd.ChatCompletionInferenceParams(
temperature=0.5, top_p=0.9, max_tokens=1600,
),
)
model_provider = dd.ModelProvider(
name="nvidia",
endpoint="https://integrate.api.nvidia.com/v1",
provider_type="openai",
api_key=os.environ["NVIDIA_API_KEY"],
)
config_builder = dd.DataDesignerConfigBuilder(model_configs=[model_config])
# Add columns as needed...
stage = DataDesignerStage(
config_builder=config_builder,
model_providers=[model_provider],
)

NDD-Backed Nemotron-CC Stages

The Nemotron-CC synthetic data stages have NDD-backed equivalents that replace the AsyncOpenAIClient with NDD execution. These stages accept the same input_field, output_field, and prompt parameters, but route generation through DataDesignerStage internally.

The following stages provide NDD-backed implementations:

StageImport PathOutput Field
WikipediaParaphrasingStagenemo_curator.stages.synthetic.nemotron_cc.nemo_data_designer.nemotron_ccrephrased
DiverseQAStagenemo_curator.stages.synthetic.nemotron_cc.nemo_data_designer.nemotron_ccdiverse_qa
DistillStagenemo_curator.stages.synthetic.nemotron_cc.nemo_data_designer.nemotron_ccdistill
ExtractKnowledgeStagenemo_curator.stages.synthetic.nemotron_cc.nemo_data_designer.nemotron_ccextract_knowledge
KnowledgeListStagenemo_curator.stages.synthetic.nemotron_cc.nemo_data_designer.nemotron_ccknowledge_list

These stages inherit from NDDBaseSyntheticStage, which auto-builds an NDD config from the prompt fields. You configure the LLM through model_configs and model_providers instead of an AsyncOpenAIClient:

import os
import data_designer.config as dd
from nemo_curator.stages.synthetic.nemotron_cc.nemo_data_designer.nemotron_cc import DiverseQAStage
model_config = dd.ModelConfig(
alias="nvidia/nemotron-3-super-120b-a12b",
model="nvidia/nemotron-3-super-120b-a12b",
provider="nvidia",
inference_parameters=dd.ChatCompletionInferenceParams(
temperature=0.5, top_p=0.9, max_tokens=1600,
),
)
model_provider = dd.ModelProvider(
name="nvidia",
endpoint="https://integrate.api.nvidia.com/v1",
provider_type="openai",
api_key=os.environ["NVIDIA_API_KEY"],
)
stage = DiverseQAStage(
input_field="text",
output_field="diverse_qa",
model_alias="nvidia/nemotron-3-super-120b-a12b",
model_configs=[model_config],
model_providers=[model_provider],
)

YAML Configuration

Instead of building configs in Python, you can define the entire NDD configuration in a YAML file and pass it to DataDesignerStage:

stage = DataDesignerStage(data_designer_config_file="config.yaml")

This is useful for reproducible pipelines where the generation config is versioned alongside data artifacts.


Next Steps

Refer to the following pages for related configuration and workflows: