Megatron-LM Fine-Tuning Deep Dive: Qwen Recipes, Datasets, and the Training Loop
A code-level tour of Megatron fine-tuning: where Qwen recipes live, how pretraining and SFT datasets are built, how pretrain_gpt.py reaches the forward/backward schedule, which extension points matter, and a successful eight-GPU Qwen-style MCore smoke test with TP, PP, CP, and EP all set to one.
Why this post
Megatron is often introduced as a collection of parallelism techniques:
- tensor parallelism;
- pipeline parallelism;
- context parallelism;
- expert parallelism;
- distributed optimizer and data parallelism.
That description is correct, but it is not the best place to start if the task is fine-tuning a model.
For fine-tuning, the more useful questions are:
- Where is the model architecture selected?
- What does the dataset have to look like?
- Which function owns the training loop?
- Where can I replace the model, forward step, loss, optimizer, or data source?
- Can Megatron run without TP, PP, CP, or EP?
This post answers those questions by following the open-source code.
All Megatron-LM links are pinned to commit 78901d8a. Qwen-specific recipe links are pinned separately to Megatron-Bridge commit fcbb6031.
1. Megatron-LM and Megatron-Bridge are different layers
The first important distinction is repository scope.
Megatron-LM
NVIDIA/Megatron-LM contains:
- Megatron Core model building blocks under
megatron/core; - the high-level
pretrain_gpt.pyentrypoint; - distributed initialization;
- dataset builders;
- pipeline schedules;
- optimizer and checkpoint infrastructure.
Its GPTModel is architecture-configurable. It is not organized as one Python model class per Hugging Face model family.
Megatron-Bridge
NVIDIA-NeMo/Megatron-Bridge adds the model-specific and Hugging Face-facing layer:
- Qwen, Llama, DeepSeek, and other recipes;
- Hugging Face to Megatron conversion;
- pretraining, SFT, and PEFT configurations;
- higher-level training configuration containers.
This matters because Megatron-LM itself has no dedicated examples/qwen directory. The official Qwen recipes live in Megatron-Bridge.
For example, the Bridge recipe qwen3_600m_pretrain_config() constructs a Qwen3 0.6B provider and explicitly sets:
1
2
3
4
cfg.model.tensor_model_parallel_size = 1
cfg.model.pipeline_model_parallel_size = 1
cfg.model.context_parallel_size = 1
cfg.model.sequence_parallel = False
The corresponding qwen3_600m_sft_config() is also designed for TP=1 and PP=1 on one node.
So Megatron does not require model parallelism. If every model-parallel size is one, eight GPUs simply form an eight-rank data-parallel group.
2. What a Qwen fine-tuning case looks like
A supervised fine-tuning run has three pieces:
1
2
3
pretrained checkpoint
+ chat-style dataset
+ the same causal language-model training loop
The Qwen Bridge SFT recipe configures a Qwen model provider, tokenizer, sequence length, batch size, optimizer precision, DDP behavior, and checkpoint locations. The recipe leaves a clear checkpoint extension point:
1
# cfg.checkpoint.pretrained_checkpoint = "/path/to/checkpoint"
The distinction between pretraining and fine-tuning is therefore not a separate distributed engine. It is primarily:
- which checkpoint initializes the model;
- which dataset class produces tokens and labels;
- which tokens are included in the loss mask;
- which optimization schedule is selected.
Underneath the recipe layer, both cases eventually reach the same Megatron forward/backward scheduler.
3. Dataset creation: pretraining versus SFT
Megatron supports two very different data paths.
3.1 Pretraining data: JSONL to .bin and .idx
The classic pretraining pipeline starts with JSONL:
1
{"text": "A document to tokenize and train on."}
The official preprocessing entrypoint is tools/preprocess_data.py. Its Encoder parses one JSON object, tokenizes selected keys, optionally appends EOD, and writes Megatron’s indexed dataset format.
A typical command is:
1
2
3
4
5
6
7
python tools/preprocess_data.py \
--input corpus.jsonl \
--output-prefix /data/qwen_corpus \
--tokenizer-type HuggingFaceTokenizer \
--tokenizer-model Qwen/Qwen3-0.6B \
--append-eod \
--workers 8
The result is normally:
1
2
/data/qwen_corpus_text_document.bin
/data/qwen_corpus_text_document.idx
Training then points --data-path at that prefix.
The indexed representation is useful for large pretraining corpora because it supports memory mapping, deterministic sample construction, and dataset blending without repeatedly parsing JSON.
3.2 SFT data: chat JSONL directly
Megatron-LM also has a direct SFT JSONL path. The format is documented by SFTLowLevelDataset:
1
2
3
4
5
6
7
{
"messages": [
{"role": "system", "content": "You are a careful math tutor."},
{"role": "user", "content": "What is 48 + 24?"},
{"role": "assistant", "content": "72"}
]
}
This path does not require .bin/.idx preprocessing. It uses the Hugging Face datasets JSON loader, and SFTDataset.__getitem__ does the important work:
- split packed conversations;
- call
tokenizer.tokenize_conversation(...); - build tokens and assistant targets;
- mask non-assistant targets with
IGNORE_INDEX; - truncate or pad to the configured sequence length;
- produce packed-sequence metadata such as
cu_seqlens.
For my pod experiment, I converted 32 rows from the mounted GSM8K parquet into this format:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import json
import pyarrow.parquet as pq
rows = pq.read_table(
"/shared/public/data/gsm8k/train.parquet",
columns=["extra_info"],
).slice(0, 32).to_pylist()
with open("gsm8k_megatron_sft_sample.jsonl", "w") as output:
for row in rows:
info = row["extra_info"]
json.dump(
{
"messages": [
{"role": "system", "content": "You are a careful math tutor."},
{"role": "user", "content": info["question"]},
{"role": "assistant", "content": info["answer"]},
]
},
output,
)
output.write("\n")
The resulting file was:
1
/home/jobuser/gsm8k_megatron_sft_sample.jsonl
with 32 conversations.
3.3 How the dataset class is selected
The high-level selection happens in train_valid_test_datasets_provider:
1
2
3
4
5
6
7
8
if args.sft:
dataset_type = SFTDataset
elif args.mock_data:
dataset_type = MockGPTDataset
elif args.fim_data:
dataset_type = GPTFIMDataset
else:
dataset_type = GPTDataset
That function is itself an extension point: a different callback can be passed to pretrain(...) to supply entirely different train, validation, and test datasets.
4. From pretrain_gpt.py to one optimizer step
The public training entrypoint is pretrain_gpt.py.
At a high level:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pretrain_gpt.py
-> parse and validate arguments
-> build GPT/model configuration
-> call pretrain(...)
-> initialize distributed state
-> build model and optimizer
-> build datasets and iterators
-> enter train(...)
-> train_step(...)
-> select forward/backward schedule
-> forward_step(...)
-> model(...)
-> loss_func(...)
-> finalize gradients
-> optimizer.step()
The reusable high-level loop starts at training.pretrain. The steady-state loop is train, and one optimizer iteration is orchestrated by train_step.
4.1 The forward-step contract
Megatron does not hard-code the user model call into the distributed scheduler. Instead, the scheduler receives a callback.
The GPT callback is forward_step. It:
- obtains a batch;
- derives packed-sequence parameters when needed;
- calls the model;
- returns the model output plus a partially bound loss function.
Conceptually:
1
2
3
4
5
6
7
8
9
output = model(
tokens,
position_ids,
attention_mask,
labels=labels,
packed_seq_params=packed_seq_params,
)
return output, partial(loss_func, loss_mask, model=model)
The loss implementation is loss_func. The ordinary language-model branch masks token losses and returns:
- the local loss sum;
- the number of valid tokens;
- reporting metrics.
4.2 Schedule selection
train_step calls get_forward_backward_func(). That factory chooses the schedule based on pipeline topology:
- no pipeline parallelism: ordinary forward/backward;
- pipeline parallelism: warmup, steady-state 1F1B, cooldown;
- virtual pipeline stages: interleaved schedule.
The callback contract stays the same. Changing PP changes the schedule, not the model-facing forward_step API.
5. The main extension points
Megatron is easier to understand when viewed as dependency injection around a distributed loop.
5.1 Model construction
The top-level factory is model_provider. It delegates to a model builder.
The default GPT builder is gpt_builder, which selects the transformer layer specification and creates GPTModel.
Useful model extension points include:
- a custom model builder;
- a custom
TransformerLayerSpec; - Transformer Engine versus local layer implementations;
- hybrid-model builders;
- custom embedding and output layers.
5.2 Dataset provider
Replace train_valid_test_datasets_provider to inject a different dataset family, sampling strategy, or storage backend.
The callback returns:
1
(train_dataset, validation_dataset, test_dataset)
5.3 Forward and loss
Replace the forward_step callback to change:
- batch transformation;
- multimodal inputs;
- auxiliary outputs;
- model call signature;
- loss closure.
Replace the loss closure to implement distillation, multiple objectives, custom token weighting, or task-specific reporting.
5.4 Optimizer
The optimizer factory is get_megatron_optimizer.
This layer selects among ordinary and distributed optimizer implementations, precision behavior, parameter groups, and optimizer wrappers.
5.5 Parallel topology
The core distributed extension point is topology:
1
world size = DP x TP x PP x CP x EP
Important command-line controls are:
1
2
3
4
5
6
7
--tensor-model-parallel-size
--pipeline-model-parallel-size
--context-parallel-size
--expert-model-parallel-size
--sequence-parallel
--use-distributed-optimizer
--use-megatron-fsdp
Setting TP=PP=CP=EP=1 does not disable Megatron. It leaves model weights unsharded and uses the remaining ranks for data parallelism.
6. Running the official simple loop as a Qwen-style model
Megatron-LM includes examples/run_simple_mcore_train_loop.py. It is the smallest end-to-end example of:
- distributed initialization;
- model construction;
MockGPTDataset;- forward/backward schedule selection;
- DDP gradient synchronization;
- optimizer stepping;
- distributed checkpoint save/load.
The original example uses TP=2 and a toy two-layer model. For the pod smoke test, I kept its loop but changed two things:
- model parallelism was set to TP=PP=CP=EP=1;
model_provider()was changed to a Qwen2.5-0.5B-style architecture.
The relevant Qwen-style configuration was:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
config = TransformerConfig(
num_layers=24,
hidden_size=896,
num_attention_heads=14,
num_query_groups=2,
kv_channels=64,
ffn_hidden_size=4864,
normalization="RMSNorm",
layernorm_epsilon=1e-6,
gated_linear_unit=True,
activation_func=torch.nn.functional.silu,
add_bias_linear=False,
add_qkv_bias=True,
hidden_dropout=0.0,
attention_dropout=0.0,
bf16=True,
params_dtype=torch.bfloat16,
pipeline_dtype=torch.bfloat16,
transformer_impl="local",
)
model = GPTModel(
config=config,
transformer_layer_spec=get_gpt_layer_local_spec(),
vocab_size=151936,
max_sequence_length=128,
position_embedding_type="rope",
rotary_base=1000000,
share_embeddings_and_output_weights=False,
)
This is a random-initialized Qwen-style smoke model, not a converted pretrained Qwen checkpoint. Loading actual Hugging Face Qwen weights is the job of the Bridge/conversion layer.
The distributed setup was:
1
2
3
4
5
6
parallel_state.initialize_model_parallel(
tensor_model_parallel_size=1,
pipeline_model_parallel_size=1,
context_parallel_size=1,
expert_model_parallel_size=1,
)
and the launch command was:
1
2
3
4
CUDA_DEVICE_MAX_CONNECTIONS=1 \
PYTHONPATH=/home/jobuser/Megatron-LM-78901d8a:$PYTHONPATH \
torchrun --standalone --nproc_per_node=8 \
/home/jobuser/run_simple_qwen_mcore.py
The pinned source tree was required because the installed wheel did not include the dataset helper Makefile used by compile_helpers().
6.1 Observed result
The model ran three forward/backward/optimizer iterations successfully:
1
2
3
4
iteration=0 losses=[{'lm loss': tensor(12.1715, device='cuda:0')}]
iteration=1 losses=[{'lm loss': tensor(12.1171, device='cuda:0')}]
iteration=2 losses=[{'lm loss': tensor(12.1635, device='cuda:0')}]
Qwen-style MCore smoke test completed.
The entire launch, including C++ dataset-helper compilation and process startup, completed in roughly 32 seconds on eight H100 GPUs.
The important result was not benchmark performance. It was proving that the ordinary Megatron Core loop works with:
1
2
3
4
5
TP=1
PP=1
CP=1
EP=1
DP=8
No model parallelism was necessary.
7. Practical fine-tuning checklist
For a real Qwen SFT job, I would verify the following in order:
- Checkpoint conversion
- Use Megatron-Bridge or another verified converter.
- Confirm QKV ordering, GQA groups, vocabulary padding, and tied/untied embeddings.
- Tokenizer
- Use the tokenizer corresponding to the checkpoint.
- Confirm pad, EOD, BOS, and EOS IDs.
- Verify the chat template used to generate labels.
- Dataset
- Store one
messageslist per JSONL row. - Confirm only assistant tokens contribute to loss.
- Inspect truncation and packing statistics.
- Store one
- Topology
- Start with TP=PP=CP=EP=1 if the model fits.
- Scale model parallelism only when memory or throughput requires it.
- Batching
- Distinguish microbatch size, global batch size, and gradient accumulation.
- Ensure global batch size is divisible by data-parallel world size.
- Numerics
- Validate BF16/FP8 choices.
- Compare initial losses before and after checkpoint conversion.
- Check gradient norms and loss-mask token counts.
- Checkpointing
- Test one save/load round trip before a long run.
- Confirm optimizer and RNG state behavior for resume.
8. Takeaways
The useful mental model is:
1
2
3
Megatron Core = distributed model and training machinery
Megatron-LM training = reusable pretrain/train/train_step orchestration
Megatron-Bridge = model-specific recipes and HF conversion
For fine-tuning:
- the SFT dataset determines which tokens carry loss;
forward_stepandloss_funcdefine the task;- the model provider defines the architecture;
- the optimizer factory defines update behavior;
- TP, PP, CP, and EP define topology, but each can remain one;
- data parallelism still gives multi-GPU scaling.
Megatron is not synonymous with model parallelism. It is a modular distributed training system that also works as an ordinary data-parallel trainer when the model fits on one GPU.
References
- Megatron-LM pinned source: https://github.com/NVIDIA/Megatron-LM/tree/78901d8a71b92ed19e3e31e00815e6bde558e9de
- Megatron-Bridge pinned source: https://github.com/NVIDIA-NeMo/Megatron-Bridge/tree/fcbb6031103d0ca845c1a54d4fee55ecfcca17b6
- Qwen3 0.6B pretraining recipe: https://github.com/NVIDIA-NeMo/Megatron-Bridge/blob/fcbb6031103d0ca845c1a54d4fee55ecfcca17b6/src/megatron/bridge/recipes/qwen/qwen3.py#L27-L106
- Qwen3 0.6B SFT recipe: https://github.com/NVIDIA-NeMo/Megatron-Bridge/blob/fcbb6031103d0ca845c1a54d4fee55ecfcca17b6/src/megatron/bridge/recipes/qwen/qwen3.py#L505-L592
- Megatron GPT entrypoint: https://github.com/NVIDIA/Megatron-LM/blob/78901d8a71b92ed19e3e31e00815e6bde558e9de/pretrain_gpt.py
- Megatron training loop: https://github.com/NVIDIA/Megatron-LM/blob/78901d8a71b92ed19e3e31e00815e6bde558e9de/megatron/training/training.py
- SFT dataset: https://github.com/NVIDIA/Megatron-LM/blob/78901d8a71b92ed19e3e31e00815e6bde558e9de/megatron/training/datasets/sft_dataset.py
- Dataset preprocessing: https://github.com/NVIDIA/Megatron-LM/blob/78901d8a71b92ed19e3e31e00815e6bde558e9de/tools/preprocess_data.py
- Simple MCore loop: https://github.com/NVIDIA/Megatron-LM/blob/78901d8a71b92ed19e3e31e00815e6bde558e9de/examples/run_simple_mcore_train_loop.py