Fix v1 grad norm and lr log#10640
Conversation
clip_grad_norm_ on FSDP2 sharded DTensor parameters returns a per-rank local shard norm (== global_norm / sqrt(shard_size)) because _get_total_norm does not all-reduce squared norms across the fsdp mesh. .item() therefore reads only the local partial, so reported grad_norm scales as 1/sqrt(dp_size) (e.g. 8xdp mbs1 vs 4xdp mbs2 differ by sqrt(2)), and the clip coefficient is applied per-shard -- corrupting the update once max_grad_norm is enabled. Compute the true global norm via get_total_norm + full_tensor (which all-reduces across the fsdp shard mesh), then clip with that scalar via clip_grads_with_norm_. The previous cp_size>1 cross-CP all-reduce is dropped: under the default mp_shard_size=world_size the fsdp mesh already spans CP, so it would over-count grad_norm by sqrt(cp_size) and skew the clip coefficient.
learning_rate was formatted with :.4f, so values below 1e-4 (e.g. 1e-5, 2e-5) printed as "0.0000". Use :.4g for learning_rate so 1e-5 shows as "1e-05" while 1e-4 still shows "0.0001". Other floats (loss, grad_norm) keep :.4f.
In apply_sequence_parallel, the try branch assigned num_key_value_heads from model.config.num_attention_heads (typo -- both read num_attention_heads), so the cp_size divisibility assert for KV heads validated the wrong value. The except branch (text_config path) was already correct.
There was a problem hiding this comment.
Code Review
This pull request introduces several improvements and bug fixes: it updates gradient norm calculation and clipping in base_trainer.py to correctly handle FSDP2 DTensor shards; fixes a typo in sequence_parallel.py where num_key_value_heads was incorrectly assigned num_attention_heads; and refactors logging_callback.py to format small learning rates using scientific notation (:.4g) instead of rounding them to zero. Feedback on the pull request points out that torch.nn.utils.get_total_norm is not a public function and will raise an AttributeError, suggesting the use of the private helper _get_total_norm from torch.nn.utils.clip_grad instead.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| from torch.distributed.tensor import DTensor | ||
|
|
||
| grads = [p.grad for p in self.model.parameters() if p.grad is not None] | ||
| total_norm = torch.nn.utils.get_total_norm(grads) |
There was a problem hiding this comment.
Calling torch.nn.utils.get_total_norm will raise an AttributeError because get_total_norm is not a public function in torch.nn.utils. The correct private helper is _get_total_norm and it must be imported from torch.nn.utils.clip_grad.
from torch.distributed.tensor import DTensor
from torch.nn.utils.clip_grad import _get_total_norm
grads = [p.grad for p in self.model.parameters() if p.grad is not None]
total_norm = _get_total_norm(grads)
What does this PR do?
Fixes three small bugs in the v1 trainer, all found while debugging cross-layout (different
dp_size/cp_size) precision alignment.1.
grad_normmis-reported (and mis-clipped) under FSDP2 sharded paramsFile:
src/llamafactory/v1/core/base_trainer.pyUnder FSDP2, parameters/gradients are sharded
DTensors across the fsdp mesh, so each rank holds only1/shard_sizeof every parameter.torch.nn.utils.clip_grad_norm_internally calls_get_total_norm, which computes the norm over the local shard only and does not all-reduce squared norms across the mesh..item()therefore reads the local partial, which equalsglobal_norm / sqrt(shard_size).Consequences:
grad_normscales as1/sqGPUmbs1vs 4-GPUmbs2(same global batch,cp_size=1) differ by exactlysqrt(2)(=sqrt(dp8/dp4)), whileloss` is identical.max_norm / total_normis also computed from the local shard norm, so oncemax_grad_normis actually enabled the gradient is clipped per-shard (different coefficient per rank), corrupting the update.Fix: compute the local norm via
get_total_norm, reduce it to the true global norm with.full_tensor()(which all-reduces across the fsdp shard mesh), then clip with that scalar viaclip_grads_with_norm_..item()is r clipping.The previous
cp_size > 1cross-CP all-reduce ofgrad_normis removed: under the defaultmp_shard_size = world_sizethe fsdp mesh already spter has already summed gradientsacross CP — the extra reduce would over-countgrad_normbysqrt(cp_size)and skew the clip coefficient.Verification: a 2-process DTensor test confirms
get_total_norm(...).item()returns the local shard nowhile.full_tensor().item()returns he fix, 8-GPU-mbs1and 4-GPU-mbs2report the samegrad_norm.2.
learning_ratelog truncation for small valuesFile:
src/llamafactory/v1/utils/callbacks/logging_callback.pylearning_ratewas formatted with:.4f, so values below1e-4(e.g.1e-5,2e-5) printed as0.0000. Use:.4gforlearning_rateso1e-5shows as1e-05while1e-4still shows0.0001. Other floats (loss,grad_norm) kee3.
num_key_value_headstypo in ulysses sequence-parallel setupFile:
src/llamafactory/v1/pluginsn/sequence_parallel.pyIn
apply_sequence_parallel, thetrybranch assigned bothnum_attention_headsandnum_key_value_headsfrommodel.conf — the second should benum_key_value_heads). As a result thecp_sizedivisibility assertion for KV heads validated the wrong value. Theexceptbranch (thetext_config` path) was already correct.Fixes #N/A (no linked issue; found during internal precision debugging)
Before submitting