Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion examples/configs/dpo.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,6 @@ policy:
grad_reduce_in_fp32: false
overlap_grad_reduce: true
overlap_param_gather: true
average_in_collective: true
data_parallel_sharding_strategy: "optim_grads_params"

data:
Expand Down
1 change: 0 additions & 1 deletion examples/configs/grpo_math_1B.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,6 @@ policy:
grad_reduce_in_fp32: false
overlap_grad_reduce: true
overlap_param_gather: true
average_in_collective: true
use_custom_fsdp: false
data_parallel_sharding_strategy: "optim_grads_params"

Expand Down
1 change: 0 additions & 1 deletion examples/configs/grpo_math_1B_megatron.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,6 @@ policy:
grad_reduce_in_fp32: false
overlap_grad_reduce: true
overlap_param_gather: true
average_in_collective: true
use_custom_fsdp: false
data_parallel_sharding_strategy: "optim_grads_params"

Expand Down
1 change: 0 additions & 1 deletion examples/configs/rm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,6 @@ policy:
grad_reduce_in_fp32: false
overlap_grad_reduce: true
overlap_param_gather: false
average_in_collective: true
data_parallel_sharding_strategy: "optim_grads_params"


Expand Down
1 change: 0 additions & 1 deletion examples/configs/sft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,6 @@ policy:
grad_reduce_in_fp32: false
overlap_grad_reduce: true
overlap_param_gather: true
average_in_collective: true
data_parallel_sharding_strategy: "optim_grads_params"
use_custom_fsdp: false

Expand Down
1 change: 0 additions & 1 deletion examples/configs/sft_openmathinstruct2_megatron.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ policy:
activation_checkpointing: false
context_parallel_size: 1
distributed_data_parallel_config:
average_in_collective: true
data_parallel_sharding_strategy: optim_grads_params
grad_reduce_in_fp32: true
overlap_grad_reduce: true
Expand Down
1 change: 0 additions & 1 deletion examples/configs/vlm_grpo_3B.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ policy:
grad_reduce_in_fp32: false
overlap_grad_reduce: true
overlap_param_gather: true
average_in_collective: true
use_custom_fsdp: false
data_parallel_sharding_strategy: "optim_grads_params"

Expand Down
1 change: 0 additions & 1 deletion examples/configs/vlm_grpo_3B_megatron.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,6 @@ policy:
grad_reduce_in_fp32: false
overlap_grad_reduce: false
overlap_param_gather: true
average_in_collective: true
use_custom_fsdp: false
data_parallel_sharding_strategy: optim_grads_params
data:
Expand Down
21 changes: 18 additions & 3 deletions nemo_rl/models/policy/megatron_policy_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,21 @@ def __init__(
"https://github.com/NVIDIA-NeMo/RL/blob/bccbc377705a81a1f4b3c31ad9767bcc15f735a8/nemo_rl/algorithms/sft.py#L175-L179."
)

## These settings are required for correct gradient computations in mcore
## when calculate_per_token_loss is True, there is no scaling of the gradient in mcore,
## so we handle the scaling in nemo-rl.
## perform_initialization = True is a workaround to ensure the correct tensor parallel attributes are set
## on the TP-sharded parameters.
model_cfg.calculate_per_token_loss = True
model_cfg.perform_initialization = False

assert (
"aux_loss" not in self.model_cfg.moe_router_load_balancing_type
Comment thread
terrykong marked this conversation as resolved.
Outdated
or self.model_cfg.moe_aux_loss_coeff == 0
), (
"MoE aux loss is currently not supported due to a known but in Megatron-LM. See ## TODO: link to GH issue"
)

Comment thread
ashors1 marked this conversation as resolved.
self.megatron_cfg = ConfigContainer(
model=model_cfg,
checkpoint=checkpoint_config,
Expand All @@ -683,9 +698,9 @@ def __init__(
overlap_param_gather=self.cfg["megatron_cfg"][
"distributed_data_parallel_config"
]["overlap_param_gather"],
average_in_collective=self.cfg["megatron_cfg"][
"distributed_data_parallel_config"
]["average_in_collective"],
# we need to set average_in_collective=False with calculate_per_token_loss=True.
# otherwise, mcore throws an assertion error.
average_in_collective=False,
use_distributed_optimizer=self.cfg["megatron_cfg"]["optimizer"][
"use_distributed_optimizer"
],
Expand Down
172 changes: 172 additions & 0 deletions tests/unit/models/policy/test_megatron_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import time
from typing import Optional

import numpy as np
import pytest
import torch

Expand Down Expand Up @@ -1824,6 +1825,177 @@ def test_megatron_context_parallel_training_agreement(tiny_llama_model_path):
)


@pytest.mark.hf_gated
@pytest.mark.timeout(300)
def test_megatron_gradient_norm_consistency_across_parallelism(tiny_llama_model_path):
"""Test that gradient norms are consistent across different TP and DP configurations.

This test validates that the same model produces identical gradient norms
regardless of tensor parallelism (TP) and data parallelism (DP) settings.
"""
batch_size = 8
seq_len = 64
vocab_size = 32000

# Create reproducible test data
torch.manual_seed(42)
input_ids = torch.randint(0, vocab_size, (batch_size, seq_len))
attention_mask = torch.ones(batch_size, seq_len)
input_lengths = attention_mask.sum(dim=1).to(torch.int32)
labels = torch.randint(0, vocab_size, (batch_size, seq_len))

data = BatchedDataDict(
{
"input_ids": input_ids,
"input_lengths": input_lengths,
"attention_mask": attention_mask,
"labels": labels,
"sample_mask": torch.ones(batch_size),
"token_mask": torch.ones_like(input_ids),
}
)

# Test configurations: (num_gpus, tp, pp, description)
test_configs = [
(1, 1, 1, "DP1TP1"),
(2, 1, 1, "DP2"), # Data parallel with 2 GPUs
(2, 2, 1, "TP2"), # Tensor parallel with 2 GPUs
]

grad_norms = {}
losses = {}

for num_gpus, tp, pp, desc in test_configs:
print(
f"\n=== Testing {desc} configuration (GPUs={num_gpus}, TP={tp}, PP={pp}) ==="
)

cluster = RayVirtualCluster(
name=f"test-grad-norm-{desc.lower()}",
bundle_ct_per_node_list=[num_gpus],
use_gpus=True,
num_gpus_per_node=num_gpus,
max_colocated_worker_groups=1,
)

config = create_megatron_test_config(
model_name=tiny_llama_model_path,
tp=tp,
pp=pp,
precision="float32", # Use float32 for more stable gradient comparisons
)

tokenizer = get_tokenizer(config["tokenizer"])
config["generation"] = configure_generation_config(
config["generation"], tokenizer
)

policy = Policy(
cluster=cluster,
config=config,
tokenizer=tokenizer,
init_reference_model=False,
)

# Use SimpleLoss for consistent comparison
loss_fn = NLLLoss()

try:
# Prepare for training
policy.prepare_for_training()

# Perform one forward/backward step
print(f"Performing forward/backward pass for {desc}...")
results = policy.train(data, loss_fn)

# Extract metrics
loss_tensor = results["loss"]
all_metrics = results["all_mb_metrics"]

# Verify loss is valid
assert not torch.isnan(loss_tensor).any(), (
f"Loss should not be NaN for {desc}"
)
assert not torch.isinf(loss_tensor).any(), (
f"Loss should not be Inf for {desc}"
)

# Extract gradient norm
assert "grad_norm" in all_metrics, (
f"grad_norm should be in metrics for {desc}"
)
grad_norm = all_metrics["grad_norm"]

# Store results for comparison
grad_norms[desc] = grad_norm
losses[desc] = loss_tensor.cpu().numpy()

print(f"{desc} - Loss: {loss_tensor}")
print(f"{desc} - Grad norm: {grad_norm}")

finally:
policy.shutdown()
cluster.shutdown()

# Compare gradient norms across configurations
print("\n=== Comparing gradient norms across configurations ===")

# Get reference values from DP2 configuration
# NOTE: even if TP2 config passes these tests, it doesn't necessarily imply
# there are no bugs. Sometimes bugs with grad norm are hard to catch.
reference_config = "DP1TP1"
reference_grad_norm = grad_norms[reference_config]
reference_loss = losses[reference_config]

for config_name, grad_norm in grad_norms.items():
if config_name == reference_config:
continue

print(f"\nComparing {config_name} with {reference_config}:")
print(f" {reference_config} grad norm: {reference_grad_norm}")
print(f" {config_name} grad norm: {grad_norm}")

# Compare gradient norms
if not isinstance(grad_norm, list):
grad_norm = [grad_norm]
reference_grad_norm = [reference_grad_norm]
if isinstance(grad_norm, list) and isinstance(reference_grad_norm, list):
# Handle case where grad_norm is a list (multiple microbatches)
assert len(grad_norm) == len(reference_grad_norm), (
f"Number of gradient norm values should match: {len(grad_norm)} vs {len(reference_grad_norm)}"
)

for i, (gn, ref_gn) in enumerate(zip(grad_norm, reference_grad_norm)):
grad_diff = abs(gn - ref_gn)
relative_diff = grad_diff / (ref_gn + 1e-8)
print(
f" Microbatch {i}: {ref_gn} vs {gn}, diff={grad_diff:.6f}, rel_diff={relative_diff:.6f}"
)

# Allow small differences due to floating point precision and parallelization
assert relative_diff < 0.01 or grad_diff < 1e-6, (
f"Gradient norm difference too large for microbatch {i}: "
f"{ref_gn} vs {gn} (diff={grad_diff:.6f}, rel_diff={relative_diff:.6f})"
)

# Compare losses (should also be identical for same computation)
loss_diff = np.max(np.abs(reference_loss - losses[config_name]))
relative_loss_diff = loss_diff / (np.mean(np.abs(reference_loss)) + 1e-8)
print(
f" Loss diff: {loss_diff:.6f}, relative loss diff: {relative_loss_diff:.6f}"
)

# Allow small differences in loss as well
assert relative_loss_diff < 0.01 or loss_diff < 1e-6, (
f"Loss difference too large: "
f"max diff={loss_diff:.6f}, rel_diff={relative_loss_diff:.6f}"
)

print(
"\n✓ SUCCESS: Gradient norms are consistent across all parallelization configurations!"
)


@pytest.mark.hf_gated
@pytest.mark.timeout(300)
def test_megatron_policy_flops_range_check(tiny_llama_model_path):
Expand Down
Loading