| import contextlib |
| import math |
| import os |
| from collections.abc import Generator, Iterable |
| from datetime import timedelta |
|
|
| import torch |
| import torch.distributed._functional_collectives as funcol |
| import torch.distributed.distributed_c10d as c10d |
| from torch import distributed as dist |
| from torch.distributed.device_mesh import DeviceMesh |
| from torch.distributed.tensor import DTensor |
|
|
| import matplotlib.pyplot as plt |
| from datetime import datetime |
| import torch |
|
|
|
|
| @torch.no_grad() |
| def clip_grad_norm_( |
| parameters: torch.Tensor | Iterable[torch.Tensor], |
| max_norm: float, |
| norm_type: float = 2.0, |
| error_if_nonfinite: bool = False, |
| foreach: bool | None = None, |
| pp_mesh: DeviceMesh | None = None, |
| ) -> torch.Tensor: |
| """ |
| Clip the gradient norm of an iterable of parameters. |
| |
| Gradient norm clipping requires computing the gradient norm over the entire model. |
| `torch.nn.utils.clip_grad_norm_` only computes gradient norm along DP/FSDP/TP dimensions. |
| We need to manually reduce the gradient norm across PP stages. |
| See https://github.com/pytorch/torchtitan/issues/596 for details. |
| |
| Args: |
| parameters: an iterable of Tensors or a single Tensor that will have gradients normalized |
| max_norm (float): max norm of the gradients |
| norm_type (float): type of the used p-norm. Can be ``'inf'`` for |
| infinity norm. |
| error_if_nonfinite (bool): if True, an error is thrown if the total |
| norm of the gradients from :attr:`parameters` is ``nan``, |
| ``inf``, or ``-inf``. Default: False (will switch to True in the future) |
| foreach (bool): use the faster foreach-based implementation. |
| If ``None``, use the foreach implementation for CUDA and CPU native tensors and silently |
| fall back to the slow implementation for other device types. |
| Default: ``None`` |
| pp_mesh: pipeline parallel device mesh. If not None, will reduce gradient norm across PP stages. |
| |
| Returns: |
| Total norm of the parameter gradients (viewed as a single vector). |
| |
| """ |
| grads = [p.grad for p in parameters if p.grad is not None] |
| total_norm = torch.nn.utils.get_total_norm( |
| grads, norm_type, error_if_nonfinite, foreach |
| ) |
|
|
| |
| |
| |
| |
| |
| |
| if isinstance(total_norm, DTensor): |
| |
| |
|
|
| |
| total_norm = total_norm.full_tensor() |
|
|
| total_norm **= norm_type |
| dist.all_reduce(total_norm, op=dist.ReduceOp.SUM, group=pp_mesh.get_group()) |
| total_norm **= 1.0 / norm_type |
|
|
| torch.nn.utils.clip_grads_with_norm_(parameters, max_norm, total_norm, foreach) |
| return total_norm |
|
|
|
|
| def reconstruct_full_mask(mask): |
| """ |
| Utilities for testing and visualizing attention masks in EEG transformer models. |
| |
| Functions: |
| - reconstruct_full_mask: Converts sparse block attention mask to full dense mask |
| - visualize_attention_mask: Creates and saves attention pattern visualization |
| """ |
|
|
| |
| block_structure = mask.to_dense()[0, 0] |
| full_shape = mask.shape[-1] |
| full_mask = torch.zeros(full_shape, full_shape, device=block_structure.device, dtype=torch.bool) |
|
|
| |
| block_size = mask.BLOCK_SIZE[0] |
| active_blocks = torch.where(block_structure == 1) |
|
|
| for q_block, kv_block in zip(active_blocks[0], active_blocks[1]): |
| |
| q_start, q_end = q_block * block_size, min((q_block + 1) * block_size, full_shape) |
| kv_start, kv_end = kv_block * block_size, min((kv_block + 1) * block_size, full_shape) |
|
|
| |
| q_indices = torch.arange(q_start, q_end, device=block_structure.device) |
| kv_indices = torch.arange(kv_start, kv_end, device=block_structure.device) |
| q_grid, kv_grid = torch.meshgrid(q_indices, kv_indices, indexing='ij') |
|
|
| |
| block_mask = mask.mask_mod(0, 0, q_grid.flatten(), kv_grid.flatten()) |
| block_mask = block_mask.reshape(q_end - q_start, kv_end - kv_start) |
|
|
| full_mask[q_start:q_end, kv_start:kv_end] = block_mask |
|
|
| return full_mask |
|
|
| def visualize_attention_mask(mask, sample_size=5000, title_suffix=""): |
| """ |
| Plot the attention mask. |
| Attentino mask needs to be constructed using reconstruct_full_mask() |
| """ |
| if mask is not None: |
| |
| full_mask = reconstruct_full_mask(mask) |
| mask_2d = full_mask.cpu().numpy() |
|
|
| |
| plt.figure(figsize=(10, 10)) |
| |
| display_mask = mask_2d[:sample_size, :sample_size] if mask_2d.shape[0] > sample_size else mask_2d |
| plt.imshow(display_mask, cmap='Blues', aspect='equal') |
| plt.xlabel('Key Position') |
| plt.ylabel('Query Position') |
| plt.title(f'Attention Mask') |
| plt.colorbar(label='Attention Allowed') |
|
|
| |
| |
| save_path = f"figures/attention_mask/mask_{title_suffix}.png" |
|
|
| |
| os.makedirs(os.path.dirname(save_path), exist_ok=True) |
| plt.savefig(save_path, dpi=300, bbox_inches='tight') |
| plt.close() |
| print(f"Attention mask saved to {save_path}") |
|
|
| def plot_random_samples_in_grid(data, |
| num_samples=100, |
| grid_rows=10, |
| grid_cols=10, |
| save_path='figures/enc_out_samples_grid.png', |
| title='100 Random Samples from encoder output'): |
| """ |
| Plot 100 random samples from xxx in a 10x10 grid and save as PNG. |
| """ |
| random_indices = torch.randperm(data.shape[0])[:num_samples].cpu().numpy() |
|
|
| fig, axes = plt.subplots(grid_rows, grid_cols, figsize=(20, 20)) |
| fig.suptitle(title, fontsize=16) |
|
|
| for idx, ax in enumerate(axes.flat): |
| sample_idx = random_indices[idx] |
| sample = data[sample_idx, :].float().detach().cpu().numpy() |
| ax.plot(sample) |
| ax.set_title(f'S{sample_idx}', fontsize=6) |
| ax.tick_params(labelsize=4) |
| ax.grid(True, alpha=0.3) |
| plt.tight_layout() |
| plt.savefig(save_path, dpi=150, bbox_inches='tight') |
| plt.close() |