Companion illustration for
NVIDIA-NeMo/RL #4108
— all permalinks pinned to
d044f40.
Three pages on this PR, one question each — this one: what the new path is and where it rejoins the old one.
NeMo-RL already had one way to turn a multimodal SFT dataset into training batches:
a cooker that reads the raw sample, and a task encoder that tokenizes it.
This PR adds a second family for Nemotron models — ten new cookers and one new task
encoder. The interesting part is how little of the pipeline it actually replaces.
NemotronMultiModalTaskEncoder
subclasses the generic encoder and overrides exactly two methods, then sets one flag —
and that flag is what decides who builds the training loss mask.
They merge at the task encoder itself. Packing, grouping and batching are
inherited, not rewritten. After the merge exactly one thing still differs: the flag
loss_mask_mode, which the Nemotron encoder sets to "precomputed" at
construction
and the generic one leaves as None
(default).
One conversation: 2 turns, 1 image, 7 tokens after tokenizing, of which 3 are the assistant's answer and so are the only ones training should learn from.
Both paths hand the trainer the same 7 tokens and the same 3 unmasked positions. The difference is only who computed the mask. The generic path derives it at batch time from the message roles; the Nemotron path carries it from the encoder, because its source format marks trainable spans per fragment rather than per whole turn.
| generic | nemotron | |
|---|---|---|
| Source format | messages[] plus a media manifest, in one webdataset sample | conversation[].fragments[], with separate media stores and a cache pool |
| Loss mask | derived from roles at batch time | computed in the encoder, checked at batch time |
| Media | images | images, video and audio |
| Packing | same code for both | |
They are not two versions of one thing. They read datasets produced by different
teams in different shapes, so they change for different reasons — which is why the
PR subclasses rather than adding if model_family == … branches to the
generic encoder. The cost is that a third family would need edits in more than one
place, because the typed options block
(EnergonTaskEncoderOptions)
is shared by every encoder but documents itself as Nemotron's, and the loader only
forwards it when the encoder is named nemotron_multimodal
(sft_dataloader.py:381).