LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

Your go-to resource for mastering AI & LLM systems.

Product

  • Learn
  • Glossary
  • Tracks
  • Practice
  • Blog
  • RSS

Legal

  • Terms of Service
  • Privacy Policy

© 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 178 articles completed

🛠️Computing Foundations0/9
Git, Shell, Linux for AIDocker for Reproducible AIPython for AI EngineeringNumPy and Tensor ShapesCUDA for ML TrainingMPS & Metal for ML on MacData Structures for AISQL and Data ModelingAlgorithms for ML Engineers
📊Math & Statistics0/8
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningStatistics and UncertaintyDistributions and SamplingHypothesis Tests, Intervals, and pass@k
📚Preparation & Prerequisites0/13
Neural Networks from ScratchCNNs from ScratchTraining & BackpropagationSoftmax, Cross-Entropy & OptimizationRNNs, LSTMs, GRUs, and Sequence ModelingAutoencoders and VAEsThe Transformer Architecture End-to-EndLanguage Modeling & Next TokensFrom GPT to Modern LLMsPrompt Engineering FundamentalsCalling LLM APIs in ProductionFirst AI App End-to-EndThe LLM Lifecycle
🧮ML Algorithms & Evaluation0/11
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageClustering and PCACore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingPyTorch Training LoopsDataset Pipelines and Data Quality
📦Production ML Systems0/6
Feature Engineering for Production MLBatch and Streaming Feature PipelinesGradient Boosted Trees in ProductionRanking and Recommendation SystemsForecasting and Anomaly DetectionMonitoring Predictive Models
🧪Core LLM Foundations0/8
The Bitter Lesson & ComputeBPE, WordPiece, and SentencePieceStatic to Contextual EmbeddingsPerplexity & Model EvaluationFile Ingestion for AIChunking StrategiesLLM Benchmarks & LimitationsInstruction Tuning & Chat Templates
🧰Applied LLM Engineering0/24
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernanceData Labeling and Human FeedbackEvaluating AI AgentsProduction RAG PipelinesHybrid Search: Dense + SparseReranking and Cross-Encoders for RAGRAG Evaluation for Reliable AnswersLLM-as-a-Judge EvaluationBias & Fairness in LLMsHallucination Detection & MitigationLLM Observability & MonitoringExperiment Tracking with MLflow and W&BPrompt Optimization with DSPyModel Versioning & DeploymentSemantic Caching & Cost OptimizationLLM Cost Engineering & Token EconomicsModel Gateways, Routing, and FallbacksDesign an Automated Support Agent
🎓Portfolio Capstones0/9
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned ClassifierCapstone: Reproducible ML Study
🧠Transformer Deep Dives0/8
Sentence Embeddings & Contrastive LossEmbedding Similarity & QuantizationScaled Dot-Product AttentionVision Transformers and Image EncodersPositional Encoding: RoPE & ALiBiLayer Normalization: Pre-LN vs Post-LNMechanistic InterpretabilityDecoding Strategies: Greedy to Nucleus
🧬Advanced Training & Adaptation0/17
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabJAX for PyTorch ResearchersContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineMixed Precision TrainingDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningTraining Run OperationsReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight Interpolation
🤖Advanced Agents & Retrieval0/16
Vector DB Internals: HNSW & IVFAdvanced RAG: HyDE & Self-RAGGraphRAG & Knowledge GraphsRAG Security & Access ControlStructured Output GenerationReAct & Plan-and-ExecuteGuardrails & Safety FiltersCode Generation & SandboxingComputer-Use / GUI / Browser AgentsHuman-in-the-Loop Agent ArchitectureAI Coding Workflow with AgentsAgent Memory & PersistenceAgent Failure & RecoveryRecursive Language Models (RLM)Multi-Agent OrchestrationCapstone: Production Agent
⚡Inference & Production Scale0/19
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceModel Quantization: GPTQ, AWQ & GGUFLocal LLM DeploymentSLM Specialization & Edge DeploymentSpeculative DecodingLong Context Window ManagementMixture of Experts ArchitectureMamba & State Space ModelsReasoning & Test-Time ComputeAdvanced MLOps & DevOps for AIGPU Serving & AutoscalingA/B Testing for LLMs
🏗️System Design Capstones0/9
Content Moderation SystemCode Completion SystemMulti-Tenant LLM PlatformLLM-Powered Search EngineVision-Language Models & CLIPMultimodal LLM ArchitectureDiffusion Models: Images & TextReal-Time Voice AI AgentReasoning Agent System Design
🎤AI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
🔬Project Deep Dives0/17
Deep Dive - vLLMDeep Dive - SkyRLDeep Dive - FlashAttentionDeep Dive - FlashInferDeep Dive - DeepGEMMDeep Dive - NCCLDeep Dive - MegatronDeep Dive - DeepSpeedDeep Dive - RayDeep Dive - MLflowDeep Dive - PyTorchDeep Dive - TransformersDeep Dive - SGLangDeep Dive - slimeDeep Dive - DeepEPDeep Dive - TinkerDeep Dive - Light-PEFT
Back to Topics
LearnAdvanced Training & AdaptationContinued Pretraining for Domain Shift
⚡HardFine-Tuning & Training

Continued Pretraining for Domain Shift

Learn when to keep the causal language-modeling objective and continue pretraining on domain text instead of jumping straight to SFT, and how to evaluate the trade-off against forgetting, cost, and downstream gain.

24 min read
Learning path
Step 101 of 178 in the full curriculum
JAX for PyTorch ResearchersSynthetic Data Pipelines

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

A base model can write a polite paragraph about authentication and still fail the next-token test that matters for adaptation. Give it the start of an internal runbook, If AUTH_TIMEOUT fires, the client should, and watch it wander into generic retry advice that never mentions your jitter budget, idempotency key, or on-call owner. The chat format looks fine. The domain text doesn't.

The JAX chapter treated a training step as a state transition you can replay: parameters, optimizer state, RNG keys, and metrics all cross an explicit boundary. Continued pretraining (CPT) needs that same contract. Pin the starting checkpoint, the domain mix, the schedule, and two eval lanes, then you can tell whether the run actually moved the model.

CPT keeps the causal next-token objective you used in the scratch GPT lab. What changes is the text distribution: more compute lands on your domain's language, document structure, and recurring entities.[1]Reference 1Don't Stop Pretraining: Adapt Language Models to Domains and Tasks.https://aclanthology.org/2020.acl-main.740/ The weights shift because the data shifted, not because you switched losses.

Three tools get mixed up here:

  • retrieval-augmented generation (RAG) leaves the weights frozen and injects documents at inference time. Use it when facts change often or must be cited.
  • Supervised Fine-Tuning (SFT) changes behavior, format, and tone with curated prompt-response pairs. Use it when the model already reads the domain but answers in the wrong shape.
  • Continued pretraining changes the weights with the same next-token objective so the model better fits unlabeled domain text. Use it when raw docs and runbooks still confuse the base model.

Don't rank those tools from labels alone. In Ovadia et al.'s knowledge-injection experiments, RAG beat unsupervised fine-tuning on MMLU and current-events questions, while repeated paraphrases of the same fact helped fine-tuning on a new-fact task.[2]Reference 2Fine-Tuning or Retrieval? Comparing Knowledge Injection in LLMshttps://aclanthology.org/2024.emnlp-main.15/ That's evidence for factual-update failures, not a universal ranking. CPT earns an experiment when the model poorly fits the domain text distribution, not when you only need fresher facts or a different answer format.

Side-by-side jobs for the same AUTH_TIMEOUT error code. Continued pretraining predicts the next runbook tokens after If AUTH_TIMEOUT fires, using unlabeled domain text and the causal language-modeling loss. Supervised fine-tuning trains on a labeled chat pair that asks what on-call should do and answers with a three-step retry checklist.
The same AUTH_TIMEOUT string is not one training problem. CPT asks the model to continue unlabeled runbook text. SFT asks it to answer in a chat interface. If the raw continuation is already broken, labeled Q&A won't fix the missing language.

The architecture stays put. The loss stays causal language modeling (mean per-token negative log-likelihood) over a sequence of tokens w1,…,wnw_1,\ldots,w_nw1​,…,wn​:

LCLM(θ)=−1n∑i=1nlog⁡P(wi∣w<i;θ)\mathcal{L}_{\mathrm{CLM}}(\theta) = -\frac{1}{n}\sum_{i=1}^{n} \log P(w_i \mid w_{<i}; \theta)LCLM​(θ)=−n1​i=1∑n​logP(wi​∣w<i​;θ)

Here w<i=(w1,…,wi−1)w_{<i} = (w_1, \ldots, w_{i-1})w<i​=(w1​,…,wi−1​) is the prefix context, and P(wi∣w<i;θ)P(w_i \mid w_{<i}; \theta)P(wi​∣w<i​;θ) is the model's predicted probability for token wiw_iwi​. Exponentiating this token-averaged loss yields the sequence perplexity PPL=exp⁡(LCLM)\mathrm{PPL} = \exp(\mathcal{L}_{\mathrm{CLM}})PPL=exp(LCLM​).

θ\thetaθ starts from the base checkpoint, not from random initialization. Later SFT still uses a next-token loss, but the examples are prompt-response pairs and the loss is usually applied to the response tokens. That's a different supervision shape, even when the math looks related.

Diagram showing What failed first?, Raw domain text: CPT, Fresh citable facts: RAG, and Format or tone: SFT.
What failed first?, Raw domain text: CPT, Fresh citable facts: RAG, and Format or tone: SFT.

If more than one failure shows up, start with the earliest one in the stack. A model that can't continue AUTH_TIMEOUT runbooks is not ready for assistant-style SFT on those same codes.

Diagnose the shift before spending training compute

A useful direct signal is held-out raw-text loss and its exponentiated form, perplexity. Record the base model's value on domain documents, then test whether CPT lowers it while a general-text control stays inside budget. A base model scoring worse on domain than general text is only a screening clue, because corpora can have different inherent predictability. It doesn't prove CPT will improve product tasks.

Fragmentation during tokenization is a weaker diagnostic. A fixed tokenizer may spend more tokens on unfamiliar identifiers, which raises context cost, but CPT doesn't change that tokenizer unless you redesign embeddings and retrain compatible weights. Use fertility as a corpus inspection signal, not a promise that continued pretraining will shorten tokenized documents.

The tiny longest-match tokenizer below is not a production BPE. It makes the inspection concrete: timeout is in the vocabulary, AUTH_TIMEOUT is not, so the error code falls apart into characters.

measure_tokenizer_fertility.py
1VOCAB = [ 2 "timeout", 3 "retry", 4 "client", 5 "should", 6 "with", 7 "backoff", 8 "AUTH", 9 "fires", 10 "the", 11 " ", 12] 13pieces = sorted(VOCAB, key=len, reverse=True) 14 15def tokenize(text: str) -> list[str]: 16 tokens: list[str] = [] 17 index = 0 18 while index < len(text): 19 matched = next((piece for piece in pieces if text.startswith(piece, index)), None) 20 if matched is None: 21 tokens.append(text[index]) 22 index += 1 23 else: 24 tokens.append(matched) 25 index += len(matched) 26 return tokens 27 28samples = { 29 "timeout": "timeout", 30 "AUTH_TIMEOUT": "AUTH_TIMEOUT", 31} 32 33print("slice n_pieces pieces") 34for name, text in samples.items(): 35 tokens = tokenize(text) 36 print(f"{name:<13}{len(tokens):>8} {tokens}")
Tokenizer fertility diagnostic
1slice n_pieces pieces 2timeout 1 ['timeout'] 3AUTH_TIMEOUT 9 ['AUTH', '_', 'T', 'I', 'M', 'E', 'O', 'U', 'T']

Vocabulary extension vs. fixed tokenizer

High token fertility raises context consumption and inference latency. If domain identifiers consistently fragment into character-level pieces, you face a structural choice:

  1. Keep the fixed tokenizer (standard practice): Retain the base vocabulary VVV and embedding matrix E∈RV×dE \in \mathbb{R}^{V \times d}E∈RV×d. The model adapts its attention weights and representations to domain n-grams without disrupting existing token coordinates. This is the default in models like Code Llama because it avoids initialization instability.[3]Reference 3Code Llama: Open Foundation Models for Codehttps://arxiv.org/abs/2308.12950
  2. Extend the vocabulary: Add KKK domain-specific tokens (such as frequent API methods or specialized identifiers), expanding the embedding matrix E→R(V+K)×dE \to \mathbb{R}^{(V+K) \times d}E→R(V+K)×d and the output language-modeling head Whead→R(V+K)×dW_{\mathrm{head}} \to \mathbb{R}^{(V+K) \times d}Whead​→R(V+K)×d.

If you do extend the vocabulary, don't initialize new rows with random Gaussian noise. Random vectors produce large gradient spikes that can destabilize pretrained attention layers during early steps. Instead, initialize each new token embedding with the mean of the embeddings of its constituent subwords from the base tokenizer. Every new token also needs enough occurrences in the adaptation corpus for those rows to move; a handful of AUTH_TIMEOUT hits won't do it. That's why small domain runs usually keep the base tokenizer intact.

Don't split a validation corpus by shuffled token chunks. Near-duplicates, revisions of the same OpenAPI page, or two runbook copies from the same source can land in both training and validation and make CPT look stronger than those splits justify. Assign a provenance or deduplication group to one split before tokenization.

group_domain_holdout.py
1import hashlib 2 3documents = [ 4 {"group": "auth-docs-v3", "text": "AUTH_TIMEOUT means the token exchange exceeded 2s"}, 5 {"group": "auth-docs-v3", "text": "AUTH_REJECTED means the client secret is invalid"}, 6 {"group": "webhook-runbooks", "text": "exhausted retries require owner acknowledgement"}, 7 {"group": "webhook-runbooks", "text": "duplicate deliveries must be idempotent"}, 8 {"group": "sdk-notes", "text": "SDK v4 retries AUTH_TIMEOUT with jitter"}, 9 {"group": "payments-api", "text": "capture calls must include idempotency-key"}, 10] 11 12def split_for_group(group: str) -> str: 13 bucket = int(hashlib.sha256(group.encode()).hexdigest(), 16) % 4 14 return "validation" if bucket == 0 else "train" 15 16splits = {"train": [], "validation": []} 17for doc in documents: 18 splits[split_for_group(doc["group"])].append(doc) 19 20train_groups = {doc["group"] for doc in splits["train"]} 21validation_groups = {doc["group"] for doc in splits["validation"]} 22assert train_groups.isdisjoint(validation_groups) 23 24print(f"train_groups={sorted(train_groups)}") 25print(f"validation_groups={sorted(validation_groups)}") 26print("group leakage: none")
Grouped holdout split
1train_groups=['auth-docs-v3', 'sdk-notes'] 2validation_groups=['payments-api', 'webhook-runbooks'] 3group leakage: none

Those two measurements still leave an open question: if you do start training, how hard should you push the weights?

The two failure dynamics: forgetting and underfitting

Resuming on a new distribution pulls the weights in two directions, and a good run balances them.

Catastrophic forgetting is loss of previously learned ability as parameters shift to absorb new data. Push too hard on AUTH_TIMEOUT runbooks and broad validation quality can regress.

Underfitting is the opposite failure: train too gently and the domain leaves no real impression. The model still can't continue the runbook.

Two major controls for this balance are the learning-rate schedule and the data mixture. Run length and corpus quality matter too.

Learning rate re-warming and re-decaying

A base checkpoint often finished its original cosine schedule at a very small learning rate. If you resume at that floor, adaptation may be inefficient. If you resume too aggressively, general-text loss may regress.

Ibrahim et al. (2024) study a related decoder-only continual-pretraining setting: updating a model with large new datasets after its original cosine schedule ended.[4]Reference 4Simple and Scalable Strategies to Continually Pre-train Large Language Modelshttps://openreview.net/forum?id=DimPeeCxKO For 405M models under English-to-English and English-to-German shifts, and a 10B-parameter model under the English-to-English shift, learning-rate re-warming, re-decaying, and replay matched retraining baselines on their reported losses and evaluation averages while spending less compute. Their experiment is evidence for testing this recipe, not permission to copy one peak learning rate into every domain run.

Code Llama is a useful second data point. Those models start from Llama 2 and continue pretraining on 500B code-heavy tokens (1T for the 70B). The authors kept the original Llama 2 peak learning rates (for example 3×10−43\times 10^{-4}3×10−4 at 7B and 13B) rather than shrinking them the way people often do for fine-tuning.[3]Reference 3Code Llama: Open Foundation Models for Codehttps://arxiv.org/abs/2308.12950 That agrees with Ibrahim's warning not to sit at the cosine floor. It still isn't a universal peak to paste into your schedule.

One subtlety from Ibrahim et al.: re-warming can itself increase loss on old data. Sweep the peak and measure both lanes instead of assuming adaptation is free. The paper also explores schedules that aren't tied to one fixed token budget.

The snippet below only shows the shape: climb from a small floor to a peak, then cosine-decay back. The peak is an order-of-magnitude fixture from Ibrahim's published cosine max, not a hyperparameter you should inherit.

rewarm_redecay_schedule.py
1import math 2 3def rewarm_redecay(step: int, total_steps: int, warmup_steps: int, peak: float, floor: float) -> float: 4 if step < warmup_steps: 5 return floor + (peak - floor) * (step + 1) / warmup_steps 6 progress = (step - warmup_steps) / max(1, total_steps - warmup_steps - 1) 7 cosine = 0.5 * (1.0 + math.cos(math.pi * progress)) 8 return floor + (peak - floor) * cosine 9 10total_steps = 1000 11warmup_steps = 50 12peak = 3e-4 # sweep this; do not inherit it blindly 13floor = 3e-5 14 15for step in [0, 49, 50, 250, 999]: 16 print(f"step={step:>3} lr={rewarm_redecay(step, total_steps, warmup_steps, peak, floor):.2e}")
Re-warm then re-decay schedule
1step= 0 lr=3.54e-05 2step= 49 lr=3.00e-04 3step= 50 lr=3.00e-04 4step=250 lr=2.71e-04 5step=999 lr=3.00e-05

Replay: keep prior-data signal in the mix

The second knob is replay: mix a fraction of previous or representative general-purpose data back into the incoming domain corpus. It provides training signal on broad text while the domain stream shifts the model, so it's a practical candidate for limiting regression.

The effective training objective becomes a weighted expectation across data distributions:

Lmix(θ)=(1−α) Ex∼Ddomain[LCLM(x;θ)]+α Ey∼Dreplay[LCLM(y;θ)]\mathcal{L}_{\mathrm{mix}}(\theta) = (1 - \alpha)\,\mathbb{E}_{x \sim \mathcal{D}_{\mathrm{domain}}}[\mathcal{L}_{\mathrm{CLM}}(x; \theta)] + \alpha\,\mathbb{E}_{y \sim \mathcal{D}_{\mathrm{replay}}}[\mathcal{L}_{\mathrm{CLM}}(y; \theta)]Lmix​(θ)=(1−α)Ex∼Ddomain​​[LCLM​(x;θ)]+αEy∼Dreplay​​[LCLM​(y;θ)]

where α∈[0,1]\alpha \in [0, 1]α∈[0,1] is the replay ratio.

How much replay? Treat it as a sweep, not a standard percentage. In Ibrahim et al.'s headline comparison, the chosen mixes use 5% replay for the SlimPajama update and 25% replay for the larger English-to-German shift.[4]Reference 4Simple and Scalable Strategies to Continually Pre-train Large Language Modelshttps://openreview.net/forum?id=DimPeeCxKO Code Llama's 500B code stage is 85% code, 8% natural language related to code, and 7% general natural language.[3]Reference 3Code Llama: Open Foundation Models for Codehttps://arxiv.org/abs/2308.12950 Llama 3 later branched a code expert from the main pretraining run and continued on a 1T-token mix that was mostly (>85%) code, following a similar recipe.[5]Reference 5The Llama 3 Herd of Models.https://arxiv.org/abs/2407.21783 Those values belong to those datasets and compute budgets. With a fixed token budget, replay also replaces some new-domain tokens, so it can reduce adaptation opportunity while controlling general regression.

compute_equivalent_replay.py
1total_tokens = 2_000_000 2 3print("replay_ratio domain_tokens replay_tokens total_tokens") 4for replay_ratio in [0.00, 0.05, 0.25]: 5 replay_tokens = int(total_tokens * replay_ratio) 6 domain_tokens = total_tokens - replay_tokens 7 assert domain_tokens + replay_tokens == total_tokens 8 print(f"{replay_ratio:>11.0%}{domain_tokens:>15,}{replay_tokens:>15,}{total_tokens:>14,}")
Compute-equivalent replay accounting
1replay_ratio domain_tokens replay_tokens total_tokens 2 0% 2,000,000 0 2,000,000 3 5% 1,900,000 100,000 2,000,000 4 25% 1,500,000 500,000 2,000,000

Your continued-pretraining run resumes from the base checkpoint at its final tiny learning rate and uses 100% domain text. Domain perplexity barely moves. After you raise the re-warm peak, domain perplexity improves but general-text loss regresses. Which two sweeps should you run?

Answer

Sweep a moderate re-warm peak followed by re-decay: inheriting the old floor can underfit, while an aggressive peak can damage broad behavior. Sweep replay ratios too, then select against held-out domain and general metrics. Replay is a regression-control candidate, not the explanation for the original underfitting.

When continued pretraining is the right tool

Reach for continued pretraining when the domain has its own language that the base model under-serves:

  • internal API docs and error catalogs (AUTH_TIMEOUT, AUTH_REJECTED)
  • on-call runbooks and incident notes
  • SDK guides with domain-specific method names
  • long compliance or protocol documents
  • codebases whose APIs and identifiers barely appeared in public pretraining

The trigger isn't "the product team wants custom behavior." The model needs more exposure to the domain's text distribution before post-training behavior shaping makes sense.

Good signals

SignalWhy it points to continued pretraining
Model misreads domain terminologyIt lacks token-distribution familiarity, not response style alone
Long domain documents feel unnatural to the modelThe base corpus underrepresented this text type
Raw completions are weak even before instruction formattingThe issue appears before chat behavior enters the picture
You have lots of domain text but few high-quality prompt-response labelsContinued pretraining can exploit unlabeled corpora

Bad signals

SignalBetter tool
Model knows the facts but answers in the wrong formatSFT
You need fresh, frequently changing, or citable factsRAG
Model needs one task-specific classifier headsupervised fine-tuning with a classifier head
Model is mostly correct but chooses the wrong safe vs unsafe answerpreference optimization

A practical split for the AUTH_TIMEOUT assistant: test raw domain-text continuation and prompt-response behavior separately. If the model can't continue the runbook or OpenAPI description, that points to continued pretraining. If raw continuation is competent but assistant behavior is weak, that points more directly to SFT.

The 2020 "Don't Stop Pretraining" paper made a related distinction in masked-language-model experiments with RoBERTa:[1]Reference 1Don't Stop Pretraining: Adapt Language Models to Domains and Tasks.https://aclanthology.org/2020.acl-main.740/

  • DAPT (domain-adaptive pretraining): keep training on large unlabeled domain text such as API docs or runbooks
  • TAPT (task-adaptive pretraining): continue on the task's own unlabeled inputs, even when the corpus is smaller

The decision remains useful for decoder-only LLM projects, but don't silently transfer RoBERTa's quantitative gains to a causal base model. You still have to measure whether more exposure to the target text distribution improves your model and downstream task.

Data for continued pretraining

The same discipline from large-scale pretraining still applies:

  • filter low-quality text
  • deduplicate aggressively
  • remove benchmarks and eval leakage
  • scrub PII and secrets from runbooks and traces
  • keep provenance and usage rights for every corpus slice

The corpus can be narrower and more targeted. Domain data can also be more sensitive than public pretraining text, so provenance, access control, and removal procedures are product requirements, not cleanup tasks.

Gate the corpus before tokenization

Keep a manifest that records whether a source may be trained on, whether it contains unresolved sensitive content, and whether it's reserved for evaluation. A high-quality domain document that fails one of these gates doesn't belong in the training stream.

gate_domain_manifest.py
1sources = [ 2 {"name": "public-api-docs", "tokens": 800_000, "licensed": True, "pii_scrubbed": True, "eval_only": False}, 3 {"name": "oncall-notes", "tokens": 120_000, "licensed": True, "pii_scrubbed": False, "eval_only": False}, 4 {"name": "heldout-probes", "tokens": 25_000, "licensed": True, "pii_scrubbed": True, "eval_only": True}, 5 {"name": "vendor-export", "tokens": 300_000, "licensed": False, "pii_scrubbed": True, "eval_only": False}, 6] 7 8accepted = [ 9 row for row in sources 10 if row["licensed"] and row["pii_scrubbed"] and not row["eval_only"] 11] 12rejected = [row["name"] for row in sources if row not in accepted] 13 14print(f"accepted={[row['name'] for row in accepted]}") 15print(f"training_tokens={sum(row['tokens'] for row in accepted):,}") 16print(f"rejected={rejected}")
Corpus manifest gate
1accepted=['public-api-docs'] 2training_tokens=800,000 3rejected=['oncall-notes', 'heldout-probes', 'vendor-export']

oncall-notes is licensed but still contains unresolved PII, while heldout-probes is clean and licensed. Which source can enter continued-pretraining blocks?

Answer

Neither. Unresolved PII blocks oncall-notes, and the evaluation-only flag keeps heldout-probes out of training. Corpus quality never overrides rights, privacy, or evaluation-isolation gates.

Keep evaluation text out of training

For a small exact-overlap gate, normalize text and hash it before building token blocks. Production pipelines also need near-duplicate detection, because formatting changes and partial copies will evade exact hashes.

remove_exact_eval_overlap.py
1import hashlib 2 3def fingerprint(text: str) -> str: 4 normalized = " ".join(text.lower().split()) 5 return hashlib.sha256(normalized.encode()).hexdigest() 6 7heldout = [ 8 "AUTH_TIMEOUT: token exchange exceeded 2s. Retry with jitter.", 9 "Webhook retries above 8 require owner acknowledgement.", 10] 11candidate_training = [ 12 "SDK v4 retry notes for AUTH_REJECTED.", 13 " auth_timeout: TOKEN exchange exceeded 2s. retry with jitter. ", 14 "Idempotency-key requirements for capture calls.", 15] 16 17heldout_hashes = {fingerprint(text) for text in heldout} 18clean_training = [ 19 text for text in candidate_training 20 if fingerprint(text) not in heldout_hashes 21] 22 23print(f"removed={len(candidate_training) - len(clean_training)}") 24print(f"kept={len(clean_training)}") 25assert all(fingerprint(text) not in heldout_hashes for text in clean_training)
Exact evaluation decontamination
1removed=1 2kept=2

Mixing strategy

Don't train on 100% domain text just because you have it. Teams usually mix:

  • a high-quality domain slice
  • a smaller replay slice of general text

That replay is one guardrail against forgetting. The exact ratio is empirical: define candidate ratios, hold total training tokens fixed, and select with domain-gain and broad-regression metrics. If the model forgets too much general language while specializing, the run overshot.

BloombergGPT is a useful contrast, not replay evidence: it was trained from scratch on 51.27% financial and 48.73% public tokens, and reports strong financial performance while remaining competitive on general-purpose benchmarks.[6]Reference 6BloombergGPT: A Large Language Model for Financehttps://arxiv.org/abs/2303.17564 It shows that corpus composition should be explicit. It doesn't identify the right CPT replay ratio for your checkpoint.

Pack blocks and preserve the mixture

CPT uses the same causal objective as base pretraining. A common loader recipe joins document token sequences with end-of-document markers and emits full blocks. The separator marks a boundary, but it doesn't prevent cross-document attention by itself. As the data-pipeline chapter explained, choose explicitly between an ordinary causal mask and a document-isolated block-diagonal mask. Small integer token sequences make separator placement inspectable.

pack_domain_token_blocks.py
1EOS = 0 2block_size = 6 3documents = [[11, 12, 13], [21, 22], [31, 32, 33, 34]] 4 5stream = [] 6for document in documents: 7 stream.extend(document + [EOS]) 8 9blocks = [ 10 stream[start:start + block_size] 11 for start in range(0, len(stream) - block_size + 1, block_size) 12] 13 14print(f"stream={stream}") 15print(f"blocks={blocks}") 16assert all(len(block) == block_size for block in blocks) 17assert EOS in blocks[0]
Packed CPT token blocks
1stream=[11, 12, 13, 0, 21, 22, 0, 31, 32, 33, 34, 0] 2blocks=[[11, 12, 13, 0, 21, 22], [0, 31, 32, 33, 34, 0]]

This small example drops an incomplete final block instead of padding it. Production loaders need an explicit remainder policy.

Once domain and replay streams are packed, make mixture selection explicit and auditable. Here each twenty-block training window uses a seeded shuffle with the requested replay count.

build_replay_mixture.py
1import random 2 3def make_window(domain_blocks: list[str], replay_blocks: list[str], replay_ratio: float, size: int) -> list[str]: 4 if not 0.0 <= replay_ratio <= 1.0: 5 raise ValueError("replay_ratio must be between 0 and 1") 6 replay_count = round(size * replay_ratio) 7 domain_count = size - replay_count 8 if len(domain_blocks) < domain_count or len(replay_blocks) < replay_count: 9 raise ValueError("not enough packed blocks for requested window") 10 chosen = domain_blocks[:domain_count] + replay_blocks[:replay_count] 11 random.Random(7).shuffle(chosen) 12 return chosen 13 14domain_blocks = [f"domain-{index}" for index in range(20)] 15replay_blocks = [f"general-{index}" for index in range(20)] 16window = make_window(domain_blocks, replay_blocks, replay_ratio=0.25, size=20) 17 18domain_count = sum(item.startswith("domain") for item in window) 19replay_count = sum(item.startswith("general") for item in window) 20print(f"domain_blocks={domain_count} replay_blocks={replay_count}") 21print(f"first_five={window[:5]}") 22assert (domain_count, replay_count) == (15, 5)
Deterministic replay mixture
1domain_blocks=15 replay_blocks=5 2first_five=['general-2', 'general-0', 'domain-11', 'general-3', 'domain-7']

Evaluation: domain gain without lying to yourself

Continued pretraining needs two evaluation lanes at the same time.

Lane 1: domain gain

Measure:

  • domain validation perplexity
  • retrieval or classification tasks in the domain
  • generation quality on held-out domain documents
  • downstream task lift after later SFT

Lane 2: general regression

Measure:

  • a small broad-language validation slice
  • a lightweight general benchmark set
  • free-form generations outside the target domain

If you only watch domain gain, you can accidentally produce a model that sounds like one AUTH_TIMEOUT runbook and forgot how to write broadly coherent language.

Evaluate loss in comparable token units. Perplexity is exp(mean negative log-likelihood), so aggregate token-level loss before exponentiating; don't average document perplexities and call the result a corpus metric.

report_domain_and_general_ppl.py
1import math 2 3base = { 4 "domain": {"negative_log_likelihood": 840.0, "tokens": 240}, 5 "general": {"negative_log_likelihood": 540.0, "tokens": 200}, 6} 7adapted = { 8 "domain": {"negative_log_likelihood": 720.0, "tokens": 240}, 9 "general": {"negative_log_likelihood": 548.0, "tokens": 200}, 10} 11 12def perplexity(metrics: dict[str, float]) -> float: 13 return math.exp(metrics["negative_log_likelihood"] / metrics["tokens"]) 14 15print("lane base_ppl adapted_ppl delta") 16for lane in ["domain", "general"]: 17 base_ppl = perplexity(base[lane]) 18 adapted_ppl = perplexity(adapted[lane]) 19 print(f"{lane:<8}{base_ppl:>9.2f}{adapted_ppl:>13.2f}{adapted_ppl - base_ppl:>7.2f}")
Two-lane perplexity report
1lane base_ppl adapted_ppl delta 2domain 33.12 20.09 -13.03 3general 14.88 15.49 0.61

Runnable checkpoint ledger

The simplest useful artifact is a checkpoint ledger. It doesn't train a model; it shows how to choose between checkpoints after a continued-pretraining sweep. Domain perplexity can improve while general text gets worse, so the chosen checkpoint needs to pass both lanes. Use general regression as a hard gate. Among survivors, rank downstream probe accuracy first and use domain perplexity as a tie-breaker. That keeps the policy visible instead of hiding trade-offs inside an arbitrary weighted score.

continued_pretraining_checkpoint_picker.py
1checkpoints = [ 2 {"name": "base", "domain_ppl": 42.0, "general_ppl": 19.2, "probe_acc": 0.62}, 3 {"name": "cpt-1k", "domain_ppl": 31.5, "general_ppl": 19.5, "probe_acc": 0.68}, 4 {"name": "cpt-4k", "domain_ppl": 27.9, "general_ppl": 20.1, "probe_acc": 0.72}, 5 {"name": "cpt-12k", "domain_ppl": 25.8, "general_ppl": 23.9, "probe_acc": 0.71}, 6] 7 8base = checkpoints[0] 9max_general_regression = 1.5 10 11print("checkpoint domain_gain general_regression probe_acc keep") 12best = None 13best_rank = None 14 15for row in checkpoints: 16 domain_gain = base["domain_ppl"] - row["domain_ppl"] 17 general_regression = row["general_ppl"] - base["general_ppl"] 18 keep = general_regression <= max_general_regression 19 rank = (row["probe_acc"], -row["domain_ppl"]) 20 21 if keep and (best_rank is None or rank > best_rank): 22 best = row 23 best_rank = rank 24 25 print( 26 f"{row['name']:<10}" 27 f"{domain_gain:>11.1f}" 28 f"{general_regression:>20.1f}" 29 f"{row['probe_acc']:>11.2f}" 30 f" {'yes' if keep else 'no'}" 31 ) 32 33print(f"chosen={best['name']}") 34print("reason=best downstream probe, then domain perplexity, inside general-regression budget")
Checkpoint trade-off ledger
1checkpoint domain_gain general_regression probe_acc keep 2base 0.0 0.0 0.62 yes 3cpt-1k 10.5 0.3 0.68 yes 4cpt-4k 14.1 0.9 0.72 yes 5cpt-12k 16.2 4.7 0.71 no 6chosen=cpt-4k 7reason=best downstream probe, then domain perplexity, inside general-regression budget
Two-lane checkpoint sweep. Domain perplexity gain rises from 0 at base to 10.5 at 1k, 14.1 at 4k, and 16.2 at 12k. General-text regression stays at 0.3 and 0.9, then jumps to 4.7 and crosses a 1.5-point gate, so 12k is rejected and 4k is the last eligible checkpoint.
The ledger and the two charts use the same four checkpoints. Domain gain (held-out domain perplexity drop) is still climbing at 12k, but general-text regression (perplexity rise) has already crossed the 1.5 gate. Among survivors, cpt-4k wins on downstream probe accuracy.

cpt-12k has the best domain perplexity but exceeds the allowed general-regression budget. cpt-4k passes the gate and has the best downstream probe accuracy among survivors. Which checkpoint wins?

Answer

Choose cpt-4k. General regression is a hard gate, so cpt-12k is ineligible. Among eligible checkpoints, downstream probe accuracy ranks first and domain perplexity only breaks ties.

Stopping rules

Because continued pretraining keeps the same objective, it can feel deceptively safe. It isn't safe by default.

Good stopping cues:

  • domain validation loss flattens
  • downstream gains after a probe SFT stop improving
  • general regressions start to outweigh domain benefits

Bad stopping cues:

  • "we still have more domain text"
  • "loss is still going down a little"

More steps aren't a free lunch once the domain shift is already absorbed.

Where it fits relative to LoRA and SFT

Compare the choices by asking what you want to change.

GoalBest first tool
Inject fresh or citable facts without retrainingRAG
Teach new domain language patternscontinued pretraining
Teach chat or task formatSFT
Run a behavior update without full-weight trainingSFT with LoRA / QLoRA adapters
Choose between multiple acceptable responsesDPO or RLHF

LoRA and QLoRA are parameter-efficient implementation choices; QLoRA also stores the frozen base model in quantized form.[7]Reference 7QLoRA: Efficient Finetuning of Quantized Language Models.https://arxiv.org/abs/2305.14314 They don't determine what supervision teaches. An adapter can be trained with a next-token domain-text objective or with prompt-response SFT. First choose objective from the failure mode, then choose full-weight or parameter-efficient training from budget and deployment constraints.

A strong training stack often looks like:

  1. base model
  2. continued pretraining on domain corpus
  3. SFT on curated prompt-response data
  4. preference optimization if needed

Not every product needs every stage. Choose the stage that matches the failure you observe. Code Llama is one shipped version of that stack: continued pretraining on code, then instruction data.[3]Reference 3Code Llama: Open Foundation Models for Codehttps://arxiv.org/abs/2308.12950 Llama 3's code expert followed the same idea, then used the expert to collect better annotations rather than treating CPT as the product surface.[5]Reference 5The Llama 3 Herd of Models.https://arxiv.org/abs/2407.21783

Common pitfalls

Using continued pretraining to fix assistant tone

  • Symptom: the model still formats answers badly after a long domain-text run.

  • Cause: the issue was interface behavior, not domain language exposure.

  • Fix: move to SFT sooner.

Over-specializing on one corpus

  • Symptom: domain completions improve, but the model becomes narrow or brittle elsewhere.

  • Cause: no replay mixture, or too many adaptation steps.

  • Fix: keep a general-text regression lane and stop earlier.

Skipping the downstream check

  • Symptom: domain perplexity improves, but the final task model barely benefits.

  • Cause: the adaptation run optimized text fit that did not transfer to the product task.

  • Fix: probe the adapted checkpoint with a small downstream SFT instead of judging only by perplexity.

Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.A base model knows authentication concepts but cannot continue an internal AUTH_TIMEOUT runbook with its jitter and idempotency rules. Which first intervention matches the failure?

Correct answer: Continue causal pretraining on cleaned domain text, then use SFT later for assistant response format.

The failure appears in raw domain continuation, so CPT addresses distribution familiarity. RAG helps fresh facts, while SFT and DPO target behavior or preferences.

2.A tokenizer splits AUTH_TIMEOUT into nine pieces while timeout is one piece. What is the conservative adaptation choice for a small domain run?

Correct answer: Keep the base tokenizer, unless a vocabulary extension is justified with enough data and compatible row initialization.

The article treats fertility as a diagnostic, not an automatic reason to change vocabulary. Small runs usually keep token coordinates stable; extensions need constituent initialization and sufficient occurrences.

3.A domain evaluation has total NLL 60 over 20 tokens and 160 over 40 tokens. What perplexity should the aggregate report use?

Correct answer: exp(220/60) = about 39.1, because token NLL must be summed before dividing by total tokens.

Perplexity exponentiates mean token NLL. Averaging document perplexities weights short and long documents incorrectly, so aggregate the numerator and denominator first.

4.Which source passes the article's continued-pretraining corpus gate?

Correct answer: Public API docs that are licensed, PII-scrubbed, and not evaluation-only.

Rights, privacy, and evaluation isolation are independent hard gates. The fixture admits only public-api-docs and rejects the other three for different reasons.

5.A re-warmed run lowers domain perplexity but raises general loss after using 100% domain text. Which next experiment is best supported?

Correct answer: Sweep moderate re-warm peaks and replay ratios under fixed total tokens, with domain and general validation lanes.

The article treats re-warming and replay as empirical controls for underfitting and forgetting. Selection needs both domain gain and a general-regression budget.

6.Why split domain documents by provenance or deduplication group before tokenization?

Correct answer: Related documents can leak near-duplicate facts across train and validation, making CPT look stronger than it is.

The article groups revisions, copies, and source families before tokenization. This protects the evaluation lane from data leakage independent of tokenizer choice.

7.The checkpoint ledger shows cpt-12k with the best domain PPL but a 4.7-point general regression, while cpt-4k has 0.72 probe accuracy and 0.9 regression. Which wins under the stated gate?

Correct answer: cpt-4k, because cpt-12k fails the regression gate and cpt-4k leads eligible downstream probes.

General regression is a hard gate. Among survivors, the article ranks downstream probe accuracy first and uses domain PPL as a tie-breaker.

8.A 2,000,000-token window uses 25% replay with a fixed total budget. How many domain and replay tokens enter that window?

Correct answer: 1,500,000 domain tokens and 500,000 replay tokens, because replay replaces one quarter of the budget.

With a fixed total, replay is 0.25 x 2,000,000 = 500,000 and the remaining 1,500,000 tokens are domain data. Adding replay on top would change compute.

9.When should a continued-pretraining run stop even if domain loss still decreases slightly?

Correct answer: When downstream gains flatten or general regressions outweigh further domain improvement.

The article recommends stopping on transfer and regression evidence, not on remaining data or a small continued loss decrease. More adaptation can overspecialize the model.

9 questions remaining.

Next Step
Continue to Synthetic Data Pipelines

Continued pretraining can move a base model toward unlabeled domain text such as API docs and runbooks, but later SFT and preference stages still need instructions, answers, and rejected answers. The next chapter is the data engine for that stage: generating, filtering, decontaminating, and versioning synthetic training examples.

PreviousJAX for PyTorch Researchers
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

Don't Stop Pretraining: Adapt Language Models to Domains and Tasks.

Gururangan, S., Marasovic, A., Swayamdipta, S., et al. · 2020 · ACL 2020

https://aclanthology.org/2020.acl-main.740/

Fine-Tuning or Retrieval? Comparing Knowledge Injection in LLMs

Ovadia, O., Brief, M., Mishaeli, M., & Elisha, O. · 2024 · EMNLP 2024

https://aclanthology.org/2024.emnlp-main.15/

Code Llama: Open Foundation Models for Code

Rozière, M., et al. · 2023

https://arxiv.org/abs/2308.12950

Simple and Scalable Strategies to Continually Pre-train Large Language Models

Ibrahim, A., Therien, B., Gupta, K., et al. · 2024 · Transactions on Machine Learning Research

https://openreview.net/forum?id=DimPeeCxKO

The Llama 3 Herd of Models.

Dubey, A., et al. · 2024 · arXiv preprint

https://arxiv.org/abs/2407.21783

BloombergGPT: A Large Language Model for Finance

Wu, S., Irsoy, O., Lu, S., et al. · 2023

https://arxiv.org/abs/2303.17564

QLoRA: Efficient Finetuning of Quantized Language Models.

Dettmers, T., et al. · 2023 · NeurIPS

https://arxiv.org/abs/2305.14314

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.