RoBERTa › Implementing RoBERTa-style Training

Michael BrenndoerferUpdated July 18, 20255 min read
Language AI HandbookAll chapters
Part 24 of 67BERT and Variants
Chapter 173RoBERTa › Implementing RoBERTa-style Training
Reading section 8 of 17 · Implementing RoBERTa-style Training · 5 minSection 8 of 17 · 5 min

Book contents

Section 8 of 17 · 5 min read

RoBERTa › Implementing RoBERTa-style TrainingOne section · Sections in this chapter8 / 17 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Let's implement the key differences between BERT and RoBERTa training. We'll focus on the data loading and masking pipeline, which captures the most important changes. Understanding the implementation concretely will reinforce the conceptual points from earlier sections: you'll see exactly how dynamic masking differs from static masking in code, how the full-sentence packing eliminates sentence boundaries, and how the training objective simplifies without NSP.

In[16]:
Code
from typing import List, Tuple

import torch


class RoBERTaDataCollator:
    """
    Data collator for RoBERTa-style pretraining.

    Key differences from BERT:
    1. No NSP - just MLM on contiguous text
    2. Dynamic masking applied fresh each batch
    3. Full sentences without artificial sentence boundaries
    """

    def __init__(
        self,
        vocab_size: int,
        mask_token_id: int,
        pad_token_id: int,
        special_token_ids: List[int],
        mask_prob: float = 0.15,
    ):
        self.vocab_size = vocab_size
        self.mask_token_id = mask_token_id
        self.pad_token_id = pad_token_id
        self.special_token_ids = set(special_token_ids)
        self.mask_prob = mask_prob

    def __call__(
        self, token_ids: torch.Tensor
    ) -> Tuple[torch.Tensor, torch.Tensor]:
        """
        Apply dynamic masking to a batch of sequences.

        Args:
            token_ids: Shape (batch_size, seq_len)

        Returns:
            masked_ids: Input with masking applied
            labels: Original tokens at masked positions, -100 elsewhere
        """
        batch_size, seq_len = token_ids.shape
        labels = token_ids.clone()
        masked_ids = token_ids.clone()

        # Create mask for special tokens (CLS, SEP, PAD) - never mask these
        special_mask = torch.zeros_like(token_ids, dtype=torch.bool)
        for special_id in self.special_token_ids:
            special_mask |= token_ids == special_id

        # Probability matrix - 0 for special tokens, mask_prob elsewhere
        probability_matrix = torch.full(token_ids.shape, self.mask_prob)
        probability_matrix[special_mask] = 0.0

        # Sample positions to mask
        masked_indices = torch.bernoulli(probability_matrix).bool()

        # Set labels: -100 for non-masked positions (ignored in loss)
        labels[~masked_indices] = -100

        # 80% of masked tokens -> [MASK]
        indices_replaced = (
            torch.bernoulli(torch.full(token_ids.shape, 0.8)).bool()
            & masked_indices
        )
        masked_ids[indices_replaced] = self.mask_token_id

        # 10% of masked tokens -> random token
        indices_random = (
            torch.bernoulli(torch.full(token_ids.shape, 0.5)).bool()
            & masked_indices
            & ~indices_replaced
        )
        random_tokens = torch.randint(
            self.vocab_size, token_ids.shape, dtype=token_ids.dtype
        )
        masked_ids[indices_random] = random_tokens[indices_random]

        # Remaining 10% stay unchanged
        return masked_ids, labels
In[17]:
Code
# Demonstrate the collator
collator = RoBERTaDataCollator(
    vocab_size=50265,  # RoBERTa vocab size
    mask_token_id=50264,  # <mask> token
    pad_token_id=1,  # <pad> token
    special_token_ids=[0, 1, 2],  # <s>, </s>, <pad>
    mask_prob=0.15,
)

# Sample batch of token IDs
torch.manual_seed(42)
sample_batch = torch.randint(100, 1000, (4, 20))  # 4 sequences, 20 tokens each

# Apply dynamic masking
masked_batch, labels = collator(sample_batch)
Out[18]:
Console
Original batch (first sequence):
[142, 267, 476, 914, 226, 435, 620, 924, 950, 813, 878, 914, 810, 154, 631, 572, 615, 995, 967, 806]

Masked batch (first sequence):
[142, 267, 476, 914, 226, 435, 620, 924, 950, 813, 878, 914, 810, 154, 50264, 572, 615, 995, 967, 50264]

Labels (first sequence):
[-100, -100, -100, -100, -100, -100, -100, 924, -100, -100, -100, -100, -100, -100, 631, -100, -100, -100, -100, 806]

Masked tokens: 3 / 20 (15.0%)

Now let's implement the full-sentences input format that RoBERTa uses instead of sentence pairs:

In[19]:
Code
class FullSentenceDataset:
    """
    Dataset that packs contiguous text into sequences without NSP.

    Unlike BERT which uses sentence pairs, RoBERTa packs as much
    contiguous text as possible into each sequence.
    """

    def __init__(
        self,
        documents: List[List[int]],  # List of tokenized documents
        max_seq_len: int = 512,
        cls_token_id: int = 0,
        sep_token_id: int = 2,
    ):
        self.max_seq_len = max_seq_len
        self.cls_token_id = cls_token_id
        self.sep_token_id = sep_token_id

        # Flatten documents into sequences of max_seq_len
        self.examples = self._create_examples(documents)

    def _create_examples(
        self, documents: List[List[int]]
    ) -> List[torch.Tensor]:
        """Pack documents into fixed-length sequences."""
        examples = []
        current_chunk = []
        current_length = 0

        # Reserve space for [CLS] and [SEP]
        target_length = self.max_seq_len - 2

        for doc in documents:
            for token_id in doc:
                current_chunk.append(token_id)
                current_length += 1

                if current_length >= target_length:
                    # Create example: [CLS] tokens [SEP]
                    example = (
                        [self.cls_token_id]
                        + current_chunk[:target_length]
                        + [self.sep_token_id]
                    )
                    examples.append(torch.tensor(example))
                    current_chunk = []
                    current_length = 0

        return examples

    def __len__(self):
        return len(self.examples)

    def __getitem__(self, idx):
        return self.examples[idx]
In[20]:
Code
# Demonstrate full-sentence packing
sample_docs = [
    list(range(100, 150)),  # Document 1: 50 tokens
    list(range(200, 280)),  # Document 2: 80 tokens
    list(range(300, 400)),  # Document 3: 100 tokens
]

dataset = FullSentenceDataset(
    documents=sample_docs,
    max_seq_len=64,  # Short for demonstration
    cls_token_id=0,
    sep_token_id=2,
)
Out[21]:
Console
Created 3 examples from 3 documents
Each example is 64 tokens

First example (showing first 20 tokens):
[0, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118]

The first example starts with the [CLS] token (ID 0), contains packed text from the documents, and would end with [SEP] (ID 2). The key insight is that RoBERTa's input format is simpler than BERT's. No segment embeddings needed for NSP, no alternating sentence A and B. Just pack as much contiguous text as possible and apply MLM.

The FullSentenceDataset implementation reveals a subtle efficiency point: by packing tokens greedily up to the maximum sequence length, you ensure that almost no sequence is padded. Every position in the input tensor is occupied by a real token. This is important for training efficiency because padded positions are skipped during the loss computation, so padding wastes both memory and computation. BERT's sentence-pair format often required padding to equalize lengths within a batch, since individual sentences vary widely in length. RoBERTa's full-sentence packing naturally produces sequences of uniform length, maximizing GPU utilization.