Repository navigation
[optim] Bind model replicas through the native gradient clipper - #5328
sanketpurandare wants to merge 1 commit into
Conversation
f432812 to
0dd3e69
Compare
0dd3e69 to
07859a0
Compare
07859a0 to
1c0ac6e
Compare
|
Rebased onto main's optimizer-owned |
Prepared with AI assistance. Reuse current main's optimizer-owned GradientClipper instead of adding engine pre-update hooks or model norm iterators. Bind existing model_parts once through the clipper's no-op extension hook after config-only construction, preserving existing custom constructor compatibility. A model-specific implementation can finalize shared gradients immediately before clipping, including optimizer CUDA graph capture/replay. The default clipper's arithmetic stays unchanged. Keep one model lifecycle seam for necessary post-initialization/checkpoint-load weight synchronization. Extend distributed norm clipping with an optional logical parameter subset while applying the same coefficient to every physical copy. Both copies remain optimizer-owned; this does not drop replica updates. Verification: 79 owning optimizer/trainer tests passed before the final native rebase continuation, including actual custom-clipper model binding, default clipping and update ordering. Required final docs/lint/distributed and refreshed review/publication gates remain pending. No new PR or merge is authorized.
1c0ac6e to
dd5f029
Compare
|
@claude review |
|
Claude finished @tianyu-l's task in 2m 44s —— View job Verdict: Request changes. The PR is less intrusive than before, but it can lose about half its core surface: DetailsWhat's justified
What can be cut
General
|
| foreach: bool | None = None, | ||
| pp_mesh: DeviceMesh | None = None, | ||
| *, | ||
| norm_parameters: Iterable[torch.Tensor] | None = None, |
There was a problem hiding this comment.
Suggestion: I'd keep this function as it is and put the logical-norm logic in MTPGradientClipper (#5329). clip_grads_with_norm_ already lets you clip with a norm you computed yourself:
replicas = [p for p in parameters if id(p) in self._replica_ids]
logical = [p for p in parameters if id(p) not in self._replica_ids]
total_norm = dist_utils.clip_grad_norm_(
logical, max_norm, foreach=True,
pp_mesh=parallelism_context.get_optional_mesh("pp"),
)
torch.nn.utils.clip_grads_with_norm_(replicas, max_norm, total_norm, foreach=True)
return total_normThat gives the same result: the replica is left out of the norm and both copies get the same scaling factor. It also removes about 27 lines from a function every model uses, along with the new empty-norm branch. That branch changes one existing behavior: when no parameter has a grad, the zero norm used to be created on CPU and is now created on parameters[0].device. Whether that change is good or bad, it shouldn't ship as a side effect of an MTP seam.
| loaded = self.checkpointer.load(step=checkpointer_config.load_step) | ||
| if loaded: | ||
| with self.parallelism_context.activate_spmd(), torch.no_grad(): | ||
| self.model_cls._synchronize_shared_parameters( | ||
| self.model_parts, self.parallelism_context | ||
| ) | ||
| return loaded |
There was a problem hiding this comment.
Question: Why is the resync after load needed? According to #5329, both physical copies keep the canonical tok_embeddings.weight FQN. DCP should therefore load the same stored tensor into both owners, so the broadcast or copy here does nothing except add a collective on every resume.
This hook also doesn't cover every load path. For example, torchft live recovery restores state through the checkpointer without calling TrainingEngine.load_checkpoint. In practice, the canonical-FQN invariant is what keeps the copies in sync. If you know a case where the copies differ after load (partial load, exclude_from_loading, HF load through sd_adapter), please name it. Otherwise I'd drop this branch and keep only the sync after init. That sync is needed, because PP ranks get distinct seeds.
| model_parts=[], | ||
| parallelism_context=SimpleNamespace(), |
There was a problem hiding this comment.
Nit: TrainingEngine.optim_step doesn't read model_parts or parallelism_context in this PR. These look like leftovers from the engine pre-update hook you removed. The same applies to the model_cls = BaseModel default in _training_loop, unless one of those tests actually goes down the load_checkpoint() -> True path.
| model_parts=[], | |
| parallelism_context=SimpleNamespace(), |
| Custom `GradientClipper` implementations keep their config-only constructor. | ||
| The optimizer calls `bind_model_parts(model_parts)` once after construction. | ||
| A model-specific clipper can bind shared parameter replicas there and finalize | ||
| their gradients immediately before norm | ||
| calculation inside the optimizer update (including CUDA graph replay). The | ||
| default binding hook is a no-op and retains the distributed clipping math. | ||
| Configure the implementation with `Optim.Config.gradient_clipper`; runtime | ||
| parameters and process groups must not be stored in serialized configs. | ||
|
|
There was a problem hiding this comment.
Nit: This paragraph mostly describes MTP: shared replicas, finalizing grads before the norm, and CUDA graph replay. #5329's MTP.md already covers the same points almost word for word. In the general config README, I'd reduce it to one sentence, e.g. "Custom GradientClippers keep a config-only constructor; Optim calls bind_model_parts(model_parts) once after construction." Leave the rest to MTP.md.
Prepared with AI assistance.
Reuse current main's optimizer-owned GradientClipper instead of adding engine
pre-update hooks or model norm iterators. Bind existing model_parts once through
the clipper's no-op extension hook after config-only construction, preserving
existing custom constructor compatibility. A model-specific implementation can finalize
shared gradients immediately before clipping, including optimizer CUDA graph
capture/replay. The default clipper's arithmetic stays unchanged.
Keep one model lifecycle seam for necessary post-initialization/checkpoint-load
weight synchronization. Extend distributed norm clipping with an optional
logical parameter subset while applying the same coefficient to every physical
copy. Both copies remain optimizer-owned; this does not drop replica updates.
Verification
Owning-commit CPU checks: 82 passed in 14.89s.
Strict relevant Sphinx builds and all non-type repository hooks passed.
Three unchanged main type findings remain attributed, not reported as a clean
full type run. External reviewer and CI acceptance are separate, still pending.
Complete-stack integration (not this PR in isolation): real four-GPU PP2/DP2/
EP2 and PP4/DP1/EP1 ran ten updates with exact model/optimizer tensor parity,
optimizer graphs and sequential validation. A fresh native PP2/PP4 checkpoint
matrix matched resumed state exactly. These are debug-model contract checks,
not 671B throughput or convergence results.
The final 207-case GPU matrix covers in-place gradients, dense attention, PP/FSDP train/eval graphs, shared-stream backward/capture and fused numerical contracts.