Skip to content

docs(tutorials): add Ministral-3-3B text embedding fine-tuning tutorial - #4154

Open
taran-dev4u wants to merge 4 commits into
NVIDIA-NeMo:mainfrom
taran-dev4u:docs/embedding-biencoder-tutorial
Open

taran-dev4u wants to merge 4 commits into
NVIDIA-NeMo:mainfrom
taran-dev4u:docs/embedding-biencoder-tutorial

Conversation

@taran-dev4u

@taran-dev4u taran-dev4u commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do ?

Adds a tutorial notebook (Text Embedding Model Fine-Tuning with NeMo AutoModel) and reference configuration for contrastive fine-tuning of mistralai/Ministral-3-3B-Instruct-2512-BF16 as a text embedding bi-encoder using TrainBiEncoderRecipe.

Fixes #2388.

Changelog

  • Added tutorials/embedding-biencoder/finetune.ipynb: walkthrough covering contrastive learning concepts, data preparation, YAML config creation, launch command, cosine similarity evaluation, and consolidated safetensors export for mistralai/Ministral-3-3B-Instruct-2512-BF16.
  • Added tutorials/embedding-biencoder/sample_retrieval_data.jsonl: sample query-positive-hard-negative triples.
  • Added tutorials/embedding-biencoder/ministral3_3b_embed_config.yaml: reference configuration for mistralai/Ministral-3-3B-Instruct-2512-BF16 with extract_submodel: language_model, average pooling, L2 normalization, and bidirectional attention.
  • Updated tutorials/README.md and docs/model-coverage/embedding/index.mdx with links to the tutorial.

Before your PR is "Ready for review"

Pre checks:

  • Make sure you read and followed Contributor guidelines
  • Did you write any new necessary tests?
  • Did you add or update any necessary documentation?

Additional Information

@copy-pr-bot

copy-pr-bot Bot commented Oct 5, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@taran-dev4u
taran-dev4u force-pushed the docs/embedding-biencoder-tutorial branch 11 times, most recently from aec57ea to affcd32 Compare October 11, 2026 00:08
@taran-dev4u
taran-dev4u marked this pull request as ready for review October 11, 2026 00:12
@svcnvidia-nemo-ci svcnvidia-nemo-ci added the waiting-on-maintainers Waiting on maintainers to respond label Oct 11, 2026
@akoumpa
akoumpa requested review from a team and rnyak October 11, 2026 00:38
@akoumpa

akoumpa commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

Thanks @taran-dev4u for the tutorial!

The @NVIDIA-NeMo/automodel_retriever_maintainers will review and provide feedback, but please allow 5-10 working days, due to conflicting priorities. Thank you.

@taran-dev4u
taran-dev4u force-pushed the docs/embedding-biencoder-tutorial branch from affcd32 to 52e8cbf Compare October 11, 2026 04:09
@svcnvidia-nemo-ci svcnvidia-nemo-ci removed the waiting-on-maintainers Waiting on maintainers to respond label Oct 11, 2026

@jgerh jgerh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Completed tech pubs review of docs/model-coverage/embedding/index.mdx and provided a few comments

Comment thread docs/model-coverage/embedding/index.mdx Outdated
Comment on lines 35 to 38

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| [Nemotron end-to-end text bi-encoder recipe](https://github.com/NVIDIA-NeMo/Nemotron/tree/main/docs/nemotron/embed) | Adapt an embedding model to your document corpus with synthetic data generation, data preparation, NeMo AutoModel fine-tuning, evaluation, export when required, and deployment. |
| [llama3_2_1b.yaml](https://github.com/NVIDIA-NeMo/Automodel/blob/main/examples/retrieval/bi_encoder/llama3_2_1b.yaml) | Bi-encoder, Llama 3.2 1B embedding model. |
| [llama_embed_nemotron_8b.yaml](https://github.com/NVIDIA-NeMo/Automodel/blob/main/examples/retrieval/bi_encoder/llama_embed_nemotron_8b/llama_embed_nemotron_8b.yaml) | Bi-encoder, Llama-Embed-Nemotron-8B reproduction recipe. |
| [ministral3_3b_instruct.yaml](https://github.com/NVIDIA-NeMo/Automodel/blob/main/examples/retrieval/bi_encoder/ministral3_3b_instruct.yaml) | Bi-encoder, Ministral3-3B recipe. |

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied in 70e5fd5, thanks!

Comment thread docs/model-coverage/embedding/index.mdx Outdated
Comment on lines 19 to 20

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check duplicate link in the Optimized Backbones table in index12.mdx, on two adjacent lines:

  • Line 19: "Llama (Bidirectional)" links to /model-coverage/embedding-models/nvidia/llama-embed-nemotron-8b.
  • Line 20: "Llama Nemotron VL" links to the same URL.

If Llama Nemotron VL has its own coverage page, the link on line 20 is the one to change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — updated line 20 in 70e5fd5 to point to /model-coverage/embedding-models/nvidia/llama-nemotron-embed-vl-1b-v2.

@svcnvidia-nemo-ci svcnvidia-nemo-ci added the waiting-on-customer Waiting on the original author to respond label Oct 12, 2026
@rnyak

rnyak commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator

@taran-dev4u thanks for this MR. There is already an end-to-end pipeline for biencoder training & deployment. It covers synthetic data generation, hard neg mining, training, deployment, thus, I'd recommend you

  • create a Ministral-3-3B based example nb, not Llama-1B based.
  • do not use "End-to-End" instead, you can change the title something like Text embedding model Fine-Tuning with NeMo AutoModel.

@taran-dev4u taran-dev4u changed the title docs(tutorials): add end-to-end embedding bi-encoder training tutorial docs(tutorials): add Ministral-3-3B text embedding fine-tuning tutorial Oct 13, 2026
@taran-dev4u

taran-dev4u commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks @rnyak! Updated in 5df164a to use mistralai/Ministral-3-3B-Instruct-2512-BF16 (ministral3_3b_embed_config.yaml) and changed the title to Text Embedding Model Fine-Tuning with NeMo AutoModel.

@svcnvidia-nemo-ci svcnvidia-nemo-ci added waiting-on-maintainers Waiting on maintainers to respond and removed waiting-on-customer Waiting on the original author to respond labels Oct 13, 2026
Signed-off-by: Taran Mamidala <mamidalataran6333@gmail.com>
Signed-off-by: Taran Mamidala <mamidalataran6333@gmail.com>
…te title

Signed-off-by: Taran Mamidala <mamidalataran6333@gmail.com>
@taran-dev4u
taran-dev4u force-pushed the docs/embedding-biencoder-tutorial branch from 5df164a to 59a3014 Compare October 16, 2026 00:08

@HuiyingLi HuiyingLi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed all five changed files at 59a3014e352b97680b9f2ec0a3214c1134bb7565, including the executable notebook, YAML, sample data, and their production callers. Reconciled the independent correctness and adversarial-test audits. The inline findings identify reproducible failures or concrete data-flow/configuration defects.

Validation:

  • 187 native CPU tests passed. Supplemental exact-tutorial checks produced 8 failing contract assertions and 11 passing controls, including corrected scheduler/batch inputs, real tiny-backbone inference/backward, and independently calculated loss objectives.
  • The original single-GPU CLI configuration failed LR-scheduler setup; only its output directory was overridden. Unchanged notebook execution on H100 reproduced the inference AttributeError.
  • Separate native full-depth smoke runs on one H100 and two H100s with FSDP2 completed a training step and consolidated checkpoint export using max_steps=1, lr_warmup_steps=0, and isolated output directories. These controls do not validate the original schedule.

Execution used this PR's source, Torch 2.14.0a0+4fdf77b940.nv26.08, Transformers 5.18.0, and Hub checkpoint revision b6d637bef2393152b3da2b2fde72eecdee30557e. CI currently provides passing DCO/title checks only. No eight-rank execution, 100-step convergence, 24-GB feasibility, HF numerical parity, checkpoint-resume correctness, or post-initialization loaded-tensor audit is claimed.

local_batch_size: 4
ckpt_every_steps: 50
val_every_steps: 25
max_steps: 100

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Set the epoch budget so the default recipe can initialize

The documented single-GPU command fails before its first training step. With the eight bundled rows, local batch 4 and global batch 16, the loader has two microbatches and the step scheduler has one optimizer step per epoch. Omitting num_epochs keeps StepSchedulerConfig's default of 10, so the LR builder computes min(10 * 1, 100) = 10 decay steps. The configured ten-step warmup then triggers assert lr_warmup_steps < lr_decay_steps. I reproduced this through the native CLI with only the output directory overridden. Set an epoch budget consistent with 100 steps; num_epochs: null passed the production scheduler control and yielded 100 windows.

Comment on lines +330 to +331
"with torch.no_grad():\n",
" q_embeds = model(**q_inputs).sentence_embedding # [N_queries, D]\n",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Use the bi-encoder's mapping-in, tensor-out API

Executing the unchanged notebook on H100 reaches this cell and raises AttributeError: 'NoneType' object has no attribute 'sentence_embedding'. BiEncoderModel.forward(input_dict=None, **kwargs) reads the positional mapping; expanding tokenizer fields leaves input_dict=None, so it returns None. With the correct input it returns the embedding tensor itself. Use q_embeds = model(q_inputs) and p_embeds = model(p_inputs). The same original expressions failed with a real tiny backbone, while the positional-call control produced finite normalized embeddings and finite nonzero gradients.

"automodel tutorials/embedding-biencoder/ministral3_3b_embed_config.yaml --nproc-per-node 1\n",
"\n",
"# Multi-GPU training with FSDP2:\n",
"automodel tutorials/embedding-biencoder/ministral3_3b_embed_config.yaml --nproc-per-node 8\n",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Supply batch sizes compatible with the eight-GPU command

This command uses eight DP ranks (tp_size=cp_size=1) with global batch 16 and local batch 4. The production scheduler rejects it with ValueError: global_batch_size (16) must be divisible by local_batch_size * dp_size (4 * 8), independently of the LR-schedule problem. The actual YAML/config builder reproduced this failure on CPU; DP1/DP2 and controls with DP8/local1 or DP8/global32 constructed successfully. Add appropriate batch overrides to the advertised command or ship compatible defaults. This is a deterministic configuration check, not an eight-rank execution result.

Comment on lines +224 to +226
"# recipe = TrainBiEncoderRecipe(cfg)\n",
"# recipe.setup()\n",
"# recipe.run()"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Make the advertised Python training block executable

Uncommenting this block first raises NameError because the notebook never imports TrainBiEncoderRecipe. After supplying the import, recipe.run() still does not exist; the native entry point calls run_train_validation_loop(). The notebook also reads its YAML from the tutorial directory, whereas that YAML's dataset path is repository-root-relative: building its dataset from the notebook directory reproduced FileNotFoundError. Import the recipe, use its supported loop method, and make the working directory/data-path handling consistent so this advertised alternative can actually start training.

"from nemo_automodel import NeMoAutoModelBiEncoder, NeMoAutoTokenizer\n",
"\n",
"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
"model_name = \"mistralai/Ministral-3-3B-Instruct-2512-BF16\"\n",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Verify the saved training result instead of reloading the base model

Step 4 is presented as verification after fine-tuning, but this value is passed directly to both loaders and always loads the original Hub checkpoint. No training output path reaches from_pretrained, so even after fixing the inference call, the displayed rankings cannot reflect any of the training updates. Resolve a selected consolidated checkpoint from Step 3 and use it for verification, with loading options appropriate to the exported text backbone rather than the original VLM. The full notebook data flow contains no checkpoint selection or override.

"from sentence_transformers import SentenceTransformer\n",
"\n",
"# Load trained model directly\n",
"embedder = SentenceTransformer(\"./output/ministral3_3b_embed_tutorial/checkpoints/model/consolidated\")\n",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Include the selected epoch/step directory in the export path

The native recipe never writes the directory passed here. BaseRecipe.save_checkpoint first appends epoch_<epoch>_step_<step> to checkpoint_dir, then the checkpointer writes model/consolidated underneath it. Both the one- and two-H100 native smoke runs exported to <checkpoint_dir>/epoch_0_step_0/model/consolidated; <checkpoint_dir>/model/consolidated was absent. Select an actual saved step directory for this loader and update the path stated above, otherwise the deployment example cannot load the exported checkpoint even after training succeeds.

attn_implementation: flash_attention_2
extract_submodel: language_model
do_distributed_inbatch_negative: false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Enable the in-batch negatives advertised by the tutorial

Despite its name, setting this flag to false also disables local in-batch negatives. TrainBiEncoderRecipe then scores each query only against its own two passages; other queries' passages never enter the loss. The README advertises training with both in-batch and hard negatives. I exercised the real recipe loss method with two queries and independently enumerated cross-entropy: false matched the own-passage objective, while true included cross-query passages and matched the in-batch objective without a distributed process group. Enable the existing flag for the advertised objective, or explicitly describe this recipe as hard-negative-only training.

@svcnvidia-nemo-ci svcnvidia-nemo-ci added waiting-on-customer Waiting on the original author to respond and removed waiting-on-maintainers Waiting on maintainers to respond labels Oct 17, 2026
Set num_epochs: null in ministral3_3b_embed_config.yaml so StepSchedulerConfig derives epochs from max_steps, and enable do_distributed_inbatch_negative. Add batch overrides to the 8-GPU CLI command. In finetune.ipynb, import TrainBiEncoderRecipe with run_train_validation_loop(), use model(inputs) in similarity evaluation, and resolve the saved step consolidated checkpoint for verification and deployment.

Signed-off-by: Taran Mamidala <mamidalataran6333@gmail.com>
@taran-dev4u

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review! Updated in 2d00b0b:

  • Set num_epochs: null and enabled do_distributed_inbatch_negative: true in ministral3_3b_embed_config.yaml.
  • Added the --step_scheduler.global_batch_size 32 override for the 8-GPU CLI command.
  • Imported TrainBiEncoderRecipe and switched to run_train_validation_loop(), with dataset path resolved relative to the config.
  • Switched to model(inputs) for inference and resolved the saved step checkpoint (epoch_0_step_100/model/consolidated) for verification and deployment.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community-request waiting-on-customer Waiting on the original author to respond

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Docs: add an easy-to-find, end-to-end embedding (bi-encoder) training tutorial

6 participants