> This page is for version Latest (default).
> For other versions, use one of these documentation indexes:
> - Latest (default): https://docs.nvidia.com/nemo-helix/latest/llms.txt
> - 0.8.0: https://docs.nvidia.com/nemo-helix/v0.8.0/llms.txt
> - 0.7.1: https://docs.nvidia.com/nemo-helix/v0.7.1/llms.txt
> - 0.7.0: https://docs.nvidia.com/nemo-helix/v0.7.0/llms.txt
> - 0.6.0: https://docs.nvidia.com/nemo-helix/v0.6.0/llms.txt
> - 0.5.1: https://docs.nvidia.com/nemo-helix/v0.5.1/llms.txt
> - 0.5.0: https://docs.nvidia.com/nemo-helix/v0.5.0/llms.txt
> - 0.4.0: https://docs.nvidia.com/nemo-helix/v0.4.0/llms.txt
> - 0.3.0: https://docs.nvidia.com/nemo-helix/v0.3.0/llms.txt
> - 0.2.0: https://docs.nvidia.com/nemo-helix/v0.2.0/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.nvidia.com/nemo-helix/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nvidia.com/nemo-helix/_mcp/server.

# Train on SPECTER or Triplet JSONL

[▶ Run in Google Colab](https://colab.research.google.com/github/NVIDIA-NeMo/nemo-helix/blob/main/docs/customizer/tutorials/embedding-triplet-dataset.ipynb)

Register an existing embedding dataset as a platform fileset so Stage 2 Automodel can train on it. This replaces the NVDocs data, not the Nemotron 3 Embed model entity.

The default recipe is [Embedding Model Customization](/documentation/customizer-reference/tutorials/embedding-customization-job), which mines hard negatives from NVDocs. SPECTER negatives are papers that the positive does not cite, not passages mined for proximity to the positive, so expect a smaller nDCG gain. SPECTER carries no retrieval evaluation split (`eval_beir`): keep a frozen NVDocs `eval_beir` fileset for Stage 4, or skip retrieval evaluation. The `validation.jsonl` created below is only for validation loss and checkpoint selection during training.

## What this notebook registers

| Automodel field    | Source                                                                             |
| ------------------ | ---------------------------------------------------------------------------------- |
| `dataset.training` | The fileset created here, holding `training.jsonl` and optional `validation.jsonl` |
| `model`            | `default/nemotron-3-embed-1b` from the embedding tutorial                          |

Rows use the triplet schema described in [dataset format](/documentation/customizer-reference/models/dataset-format):

```json
{"query": "...", "pos_doc": "...", "neg_doc": ["...", "..."]}
```

`neg_doc` is a non-empty JSON list. The cells below convert SPECTER locally and upload the result. At the fileset root, Automodel discovers `train*.jsonl` or `training*.jsonl` as training data and `val*.jsonl`, `validation*.jsonl`, or `dev*.jsonl` as optional validation data. If no validation file is present, Automodel creates a deterministic holdout from the training rows using `schedule.validation_split`, which defaults to 10%.

Equivalent CLI for the upload, once the files exist on disk:

```bash
nemo files filesets create specter-embedding-dataset --workspace default --purpose dataset --exist-ok \
  --description "SPECTER triplets as query/pos_doc/neg_doc JSONL"
nemo files upload /tmp/specter-embedding-dataset/ specter-embedding-dataset --workspace default
nemo files list specter-embedding-dataset --workspace default
```

This notebook uses the `default` workspace. If your current CLI context uses a different workspace, run `nemo config set --workspace default` first.

```python
import json
import os
from pathlib import Path
from datasets import load_dataset
from nemo_helix_plugin.client.client import NemoClient
from nemo_helix_plugin.client.errors import ConflictError
from nemo_helix_plugin.files.types import CreateFilesetRequest
from filesets import FilesetFileSystem
from nemo_helix_plugin.files.client import FilesClient

client = NemoClient.from_config()

DATASET_SIZE = int(os.environ.get("SPECTER_ROWS", "3000"))
VALIDATION_SPLIT = 0.05
SEED = 42
DATASET_PATH = Path("/tmp/specter-embedding-dataset")
DATASET_NAME = os.environ.get("SPECTER_FILESET", "specter-embedding-dataset")
os.makedirs(DATASET_PATH, exist_ok=True)

print("Downloading SPECTER...")
data = load_dataset("embedding-data/SPECTER")["train"].shuffle(seed=SEED).select(range(DATASET_SIZE))
splits = data.train_test_split(test_size=VALIDATION_SPLIT, seed=SEED)

for name, dataset in [("training", splits["train"]), ("validation", splits["test"])]:
    out = DATASET_PATH / f"{name}.jsonl"
    with out.open("w") as f:
        for row in dataset:
            triplet = {
                "query": row["set"][0],
                "pos_doc": row["set"][1],
                "neg_doc": [row["set"][2]],
            }
            if not triplet["neg_doc"][0]:
                raise ValueError("empty negative")
            f.write(json.dumps(triplet) + "\n")
    print(name, out, "rows", len(dataset))

```

```python
try:
    client.files.create_fileset(
        body=CreateFilesetRequest(
            name=DATASET_NAME,
            purpose="dataset",
            description="SPECTER triplets as query/pos_doc/neg_doc JSONL",
        ),
    ).data()
    print("Created fileset", DATASET_NAME)
except ConflictError:
    print("Fileset exists", DATASET_NAME)

fs = FilesetFileSystem(client=FilesClient.from_client(client))
fs.put(
    lpath=f"{DATASET_PATH}/",
    rpath=f"default/{DATASET_NAME}/",
    recursive=True,
)
print("Uploaded:")
print(json.dumps([f.model_dump() for f in client.files.list_files(name=DATASET_NAME).data().data], indent=2))
print("dataset.training =", f"default/{DATASET_NAME}")
print("Set that on the Automodel job in the embedding tutorial. model remains default/nemotron-3-embed-1b.")

```

## Return to Stage 2

In [Embedding Model Customization](/documentation/customizer-reference/tutorials/embedding-customization-job), keep `model="default/nemotron-3-embed-1b"` and set:

```python
dataset={"training": "default/specter-embedding-dataset"}
```

Equivalent CLI:

```bash
nemo customization automodel.jobs submit --workspace default --name embed-specter \
  --model default/nemotron-3-embed-1b \
  --dataset.training default/specter-embedding-dataset \
  --spec '{
  "training": {
    "training_type": "sft",
    "recipe": "bi_encoder",
    "finetuning_type": "all_weights",
    "max_seq_length": 512,
    "precision": "bf16",
    "attn_implementation": "flash_attention_2",
    "retrieval": {"query_max_length": 512, "passage_max_length": 512, "train_n_passages": 2, "do_distributed_inbatch_negative": false, "export": {"primary": "hf"}}
  },
  "schedule": {"epochs": 2},
  "batch": {"global_batch_size": 128, "micro_batch_size": 8},
  "optimizer": {"learning_rate": 2.5e-5, "min_learning_rate": 1.25e-5, "warmup_steps": 5, "weight_decay": 0.01, "lr_decay_style": "cosine"},
  "parallelism": {"num_gpus_per_node": 4, "tensor_parallel_size": 1},
  "output": {"name": "nemotron-3-embed-1b-specter"}
}'
```

Each SPECTER row carries one negative, so `train_n_passages` is 2 rather than the 5 used with mined NVDocs negatives. Optimizer, batch, and GPU settings match the validated NVDocs recipe, except warmup. The default split keeps 2,850 of the 3,000 SPECTER rows for training. Two epochs at global batch size 128 are about 46 optimizer steps, so this command sets `warmup_steps` to 5, about 10% of the run. Scale warmup to about 10% of total training steps whenever the training-row count or the number of epochs changes. `dataset.training` points at the whole fileset, so Automodel reads both `training.jsonl` and the optional `validation.jsonl`; no separate dataset reference is needed. Automodel reads data from a fileset, not from a local path or a Hugging Face dataset URI.

For Stage 4, pass the NVDocs `eval_beir` artifacts fileset to `retrieve-eval`.