From 7d06b137b19f82df5f0850a14def214676356476 Mon Sep 17 00:00:00 2001 From: Corey Adams Date: Fri, 27 Jun 2025 07:34:59 -0700 Subject: [PATCH 01/23] First pass updates for transformer engine support --- .../models/transolver/Physics_Attention.py | 346 ++++++++++++++++++ physicsnemo/models/transolver/transolver.py | 103 ++++-- 2 files changed, 414 insertions(+), 35 deletions(-) diff --git a/physicsnemo/models/transolver/Physics_Attention.py b/physicsnemo/models/transolver/Physics_Attention.py index 519935e900..c4b7807d7c 100644 --- a/physicsnemo/models/transolver/Physics_Attention.py +++ b/physicsnemo/models/transolver/Physics_Attention.py @@ -31,9 +31,355 @@ import torch import torch.nn as nn +import transformer_engine.pytorch as te # noqa: F401 from einops import rearrange +class Physics_Attention_Base(nn.Module): + """ + Base class for all physics attention modules. + + Implements key functionality that is common across domains: + - Slice weighting and computation + - Attention among slices + - Deslicing + - Output Projection + + Each subclass must implement it's own methods for projecting input domain tokens onto the slice space. + + Deliberately, there are not default values for any of the parameters. It's assumed you will + assign them in the subclass. + + """ + + def __init__(self, dim, heads, dim_head, dropout, slice_num): + super().__init__() + inner_dim = dim_head * heads + self.dim_head = dim_head + self.heads = heads + + self.scale = dim_head**-0.5 + + self.softmax = nn.Softmax(dim=-1) + self.dropout = nn.Dropout(dropout) + self.temperature = nn.Parameter(torch.ones([1, heads, 1, 1]) * 0.5) + + self.in_project_slice = nn.Linear(dim_head, slice_num) + for l_i in [self.in_project_slice]: + torch.nn.init.orthogonal_(l_i.weight) # use a principled initialization + + self.to_q = nn.Linear(dim_head, dim_head, bias=False) + self.to_k = nn.Linear(dim_head, dim_head, bias=False) + self.to_v = nn.Linear(dim_head, dim_head, bias=False) + + # # These are used in the transformer engine pass function: + # self.qkv_project = nn.Linear(dim_head, 3 * dim_head, bias=False) + # self.attn_fn = te.DotProductAttention(num_attention_heads=self.heads, + # kv_channels= self.dim_head, + # attention_dropout=dropout, + # qkv_format="bshd", + # softmax_scale=self.scale) + + self.to_out = nn.Sequential(nn.Linear(inner_dim, dim), nn.Dropout(dropout)) + + def project_input_onto_slices(self, x) -> tuple[torch.Tensor, torch.Tensor]: + """ + Project the input onto the slice space. + """ + raise NotImplementedError("Subclasses must implement this method") + + def compute_slices_from_projections( + self, slice_projections: torch.Tensor, fx: torch.Tensor + ) -> tuple[torch.Tensor, torch.Tensor]: + """ + Compute slice weights and slice tokens from input projections and latent features. + + Args: + slice_projections (torch.Tensor): + The projected input tensor of shape [Batch, N_heads, N_tokens, Slice_num], + representing the projection of each token onto each slice for each attention head. + fx (torch.Tensor): + The latent feature tensor of shape [Batch, N_heads, N_tokens, Head_dim], + representing the learned states to be aggregated by the slice weights. + + Returns: + tuple[torch.Tensor, torch.Tensor]: + - slice_weights: Tensor of shape [Batch, N_heads, N_tokens, Slice_num], + representing the normalized weights for each slice per token and head. + - slice_token: Tensor of shape [Batch, N_heads, Slice_num, Head_dim], + representing the aggregated latent features for each slice, head, and batch. + + Notes: + - The function first computes a temperature-scaled softmax over the slice projections to obtain slice weights. + - It then aggregates the latent features (fx) for each slice using these weights. + - The aggregated features are normalized by the sum of weights for numerical stability. + """ + # Project the latent space vectors on to the weight computation space, + # and compute a temperature adjusted softmax. + slice_weights = nn.functional.softmax( + slice_projections / torch.clamp(self.temperature, min=0.1, max=5), dim=-1 + ) # [Batch, N_heads, N_tokens, Slice_num] + + # Average the slices over the token dimension + slice_norm = slice_weights.sum(2) # [Batch, N_heads, Slice_num] + + # This does the projection of the latent space fx by the weights: + slice_token = torch.matmul(slice_weights.transpose(2, 3), fx) + + # Apply the normalization (summed weights) + slice_token = slice_token / ((slice_norm[:, :, :, None] + 1e-5)) # B H G D + + return slice_weights, slice_token + + def compute_slice_attention(self, slice_tokens: torch.Tensor) -> torch.Tensor: + """ + Compute an attention mechansism for the slices + """ + q_slice_token = self.to_q(slice_tokens) + k_slice_token = self.to_k(slice_tokens) + v_slice_token = self.to_v(slice_tokens) + + dots = torch.matmul(q_slice_token, k_slice_token.transpose(-1, -2)) * self.scale + attn = self.softmax(dots) + attn = self.dropout(attn) + out_slice_token = torch.matmul(attn, v_slice_token) # B H G D + + return out_slice_token + + def compute_slice_attention2(self, slice_tokens: torch.Tensor) -> torch.Tensor: + """ + TE implementation of slice attention + """ + + qkv = self.qkv_project(slice_tokens) + qkv = rearrange(qkv, " b h s (t d) -> t b s h d", t=3, d=self.dim_head) + q_slice_token, k_slice_token, v_slice_token = qkv.unbind(0) + + out_slice_token2 = self.attn_fn(q_slice_token, k_slice_token, v_slice_token) + out_slice_token2 = rearrange( + out_slice_token2, "b s (h d) -> b h s d", h=self.heads, d=self.dim_head + ) + + return out_slice_token2 + + def compute_slice_attention3(self, slice_tokens: torch.Tensor) -> torch.Tensor: + """ + Torch SDPA implementation of slice attention + """ + + # qkv = self.qkv_project(slice_tokens) + # qkv = rearrange(qkv, " b h s (t d) -> t b h s d", t=3, d=self.dim_head) + q_slice_token = self.to_q(slice_tokens) + k_slice_token = self.to_k(slice_tokens) + v_slice_token = self.to_v(slice_tokens) + # q_slice_token, k_slice_token, v_slice_token = qkv.unbind(0) + + out_slice_token3 = torch.nn.functional.scaled_dot_product_attention( + q_slice_token, k_slice_token, v_slice_token, is_causal=True + ) + + return out_slice_token3 + + def project_attention_outputs( + self, out_slice_token: torch.Tensor, slice_weights: torch.Tensor + ) -> torch.Tensor: + """ + Project the attended slice tokens back onto the original token space. + + Args: + out_slice_token (torch.Tensor): + The output tensor from the attention mechanism over slices, + of shape [Batch, N_heads, Slice_num, Head_dim]. + slice_weights (torch.Tensor): + The slice weights tensor of shape [Batch, N_heads, N_tokens, Slice_num], + representing the contribution of each slice to each token. + + Returns: + torch.Tensor: + The reconstructed output tensor of shape [Batch, N_tokens, N_heads * Head_dim], + representing the attended features for each token, with all heads concatenated. + + Notes: + - The function projects the attended slice tokens back to the token space using the slice weights. + - The output is reshaped to concatenate all attention heads for each token. + """ + + out_x = torch.matmul(slice_weights, out_slice_token) + out_x = rearrange(out_x, "b h n d -> b n (h d)") + return self.to_out(out_x) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + """ + Forward pass of the Physics Attention module. + + Input x should have shape of [Batch, N_tokens, N_Channels] ([B, N, C]) + """ + + # Project the inputs onto learned spaces: + x_mid, fx_mid = self.project_input_onto_slices(x) + # x_mid and fx_mid should have shapes of [B, N_head, N_tokens, Head_dim] + + # Perform the linear projection of learned latent space onto slices: + slice_projections = self.in_project_slice(x_mid) + + # Slice projections has shape [B, N_head, N_tokens, Head_dim], but head_dim may have changed! + + # Use the slice projections and learned spaces to compute the slices, and their weights: + slice_weights, slice_tokens = self.compute_slices_from_projections( + slice_projections, fx_mid + ) + + # slice_weights has shape [Batch, N_heads, N_tokens, Slice_num] + # slice_tokens has shape [Batch, N_heads, N_tokens, head_dim] + + # Apply attention to the slice tokens + # out_slice_token = self.compute_slice_attention(slice_tokens) + # out_slice_token = self.compute_slice_attention2(slice_tokens) + out_slice_token = self.compute_slice_attention3(slice_tokens) + # Shape unchanged + + # Deslice: + outputs = self.project_attention_outputs(out_slice_token, slice_weights) + + # Outputs now has the same shape as the original input x + + return outputs + + +class Physics_Attention_Irregular_Mesh_2(Physics_Attention_Base): + """ + Specialization of PhysicsAttention to Irregular Meshes + """ + + def __init__(self, dim, heads=8, dim_head=64, dropout=0.0, slice_num=64): + super().__init__(dim, heads, dim_head, dropout, slice_num) + inner_dim = dim_head * heads + + self.in_project_x = nn.Linear(dim, inner_dim) + self.in_project_fx = nn.Linear(dim, inner_dim) + + def project_input_onto_slices(self, x) -> tuple[torch.Tensor, torch.Tensor]: + """ + Project the input onto the slice space. + """ + + fx_mid = rearrange( + self.in_project_fx(x), "B N (h d) -> B h N d", h=self.heads, d=self.dim_head + ) + x_mid = rearrange( + self.in_project_x(x), "B N (h d) -> B h N d", h=self.heads, d=self.dim_head + ) + + return x_mid, fx_mid + + +class Physics_Attention_Structured_Mesh_2D_2(Physics_Attention_Base): + """ + Specialization for 2d image-like meshes + """ + + def __init__( + self, + dim, + heads=8, + dim_head=64, + dropout=0.0, + slice_num=64, + H=101, + W=31, + kernel=3, + ): # kernel=3): + super().__init__(dim, heads, dim_head, dropout, slice_num) + + inner_dim = dim_head * heads + self.H = H + self.W = W + + self.in_project_x = nn.Conv2d(dim, inner_dim, kernel, 1, kernel // 2) + self.in_project_fx = nn.Conv2d(dim, inner_dim, kernel, 1, kernel // 2) + + def project_input_onto_slices(self, x) -> tuple[torch.Tensor, torch.Tensor]: + + # Rearrange the input tokens back to an image shape: + x = rearrange(x, "b (h w) c -> b c h w", h=self.H, w=self.W) + + # Apply the projections, here they are convolutions in 2D: + input_projected_fx = self.in_project_fx(x) + input_projected_x = self.in_project_x(x) + + # Next, re-reshape the projections into token-like shapes: + input_projected_fx = rearrange( + input_projected_fx, + "b (n_heads head_dim) h w -> b n_heads (h w) head_dim", + head_dim=self.dim_head, + n_heads=self.heads, + ) + input_projected_x = rearrange( + input_projected_x, + "b (n_heads head_dim) h w -> b n_heads (h w) head_dim", + head_dim=self.dim_head, + n_heads=self.heads, + ) + + # Return the projections: + return input_projected_x, input_projected_fx + + +class Physics_Attention_Structured_Mesh_3D_2(Physics_Attention_Base): + """ + Specialization for 3D-image like meshes + """ + + def __init__( + self, + dim, + heads=8, + dim_head=64, + dropout=0.0, + slice_num=32, + H=32, + W=32, + D=32, + kernel=3, + ): + super().__init__(dim, heads, dim_head, dropout, slice_num) + + inner_dim = dim_head * heads + self.H = H + self.W = W + self.D = D + + self.in_project_x = nn.Conv3d(dim, inner_dim, kernel, 1, kernel // 2) + self.in_project_fx = nn.Conv3d(dim, inner_dim, kernel, 1, kernel // 2) + + def project_input_onto_slices(self, x) -> tuple[torch.Tensor, torch.Tensor]: + """ + Project the input onto the slice space. + """ + + x = rearrange(x, "b (h w) c -> b c h w", h=self.H, w=self.W) + + # Apply the projections, here they are convolutions: + input_projected_fx = self.in_project_fx(x) + input_projected_x = self.in_project_x(x) + + # Next, re-reshape the projections into token-like shapes: + input_projected_fx = rearrange( + input_projected_fx, + "b (n_heads head_dim) h w -> b n_heads (h w) head_dim", + head_dim=self.dim_head, + n_heads=self.heads, + ) + input_projected_x = rearrange( + input_projected_x, + "b (n_heads head_dim) h w -> b n_heads (h w) head_dim", + head_dim=self.dim_head, + n_heads=self.heads, + ) + + return input_projected_x, input_projected_fx + + class Physics_Attention_Irregular_Mesh(nn.Module): "for irregular meshes in 1D, 2D or 3D space" diff --git a/physicsnemo/models/transolver/transolver.py b/physicsnemo/models/transolver/transolver.py index d722bc0ced..875c31b350 100644 --- a/physicsnemo/models/transolver/transolver.py +++ b/physicsnemo/models/transolver/transolver.py @@ -34,14 +34,18 @@ import numpy as np import torch import torch.nn as nn -from timm.layers import trunc_normal_ +import transformer_engine.pytorch as te import physicsnemo # noqa: F401 for docs from ..meta import ModelMetaData from ..module import Module from .Embedding import timestep_embedding -from .Physics_Attention import Physics_Attention_Structured_Mesh_2D + +# from .Physics_Attention import Physics_Attention_Structured_Mesh_2D +from .Physics_Attention import ( + Physics_Attention_Structured_Mesh_2D_2 as Physics_Attention_Structured_Mesh_2D, +) ACTIVATION = { "gelu": nn.GELU, @@ -107,7 +111,7 @@ def __init__( ): super().__init__() self.last_layer = last_layer - self.ln_1 = nn.LayerNorm(hidden_dim) + self.ln_1 = te.LayerNorm(hidden_dim) self.Attn = Physics_Attention_Structured_Mesh_2D( hidden_dim, heads=num_heads, @@ -118,24 +122,34 @@ def __init__( W=W, ) - self.ln_2 = nn.LayerNorm(hidden_dim) - self.mlp = MLP( - hidden_dim, - hidden_dim * mlp_ratio, - hidden_dim, - n_layers=0, - res=False, - act=act, + self.ln_mlp1 = te.LayerNormMLP( + hidden_size=hidden_dim, + ffn_hidden_size=hidden_dim * mlp_ratio, ) + + # self.ln_2 = te.LayerNorm(hidden_dim) + # self.mlp = MLP( + # hidden_dim, + # hidden_dim * mlp_ratio, + # hidden_dim, + # n_layers=0, + # res=False, + # act=act, + # ) if self.last_layer: - self.ln_3 = nn.LayerNorm(hidden_dim) - self.mlp2 = nn.Linear(hidden_dim, out_dim) + self.ln_mlp2 = te.LayerNormLinear( + in_features=hidden_dim, out_features=out_dim + ) + # self.ln_3 = te.LayerNorm(hidden_dim) + # self.mlp2 = nn.Linear(hidden_dim, out_dim) def forward(self, fx): fx = self.Attn(self.ln_1(fx)) + fx - fx = self.mlp(self.ln_2(fx)) + fx + fx = self.ln_mlp1(fx) + fx + # fx = self.mlp(self.ln_2(fx)) + fx if self.last_layer: - return self.mlp2(self.ln_3(fx)) + return self.ln_mlp2(fx) + # return self.mlp2(self.ln_3(fx)) else: return fx @@ -192,7 +206,6 @@ def __init__( self.time_fc = nn.Sequential( nn.Linear(n_hidden, n_hidden), nn.SiLU(), nn.Linear(n_hidden, n_hidden) ) - self.blocks = nn.ModuleList( [ Transolver_block( @@ -220,10 +233,10 @@ def initialize_weights(self): def _init_weights(self, m): if isinstance(m, nn.Linear): - trunc_normal_(m.weight, std=0.02) + nn.init.trunc_normal_(m.weight, std=0.02) if isinstance(m, nn.Linear) and m.bias is not None: nn.init.constant_(m.bias, 0) - elif isinstance(m, (nn.LayerNorm, nn.BatchNorm1d)): + elif isinstance(m, (nn.LayerNorm, te.LayerNorm, nn.BatchNorm1d)): nn.init.constant_(m.bias, 0) nn.init.constant_(m.weight, 1.0) @@ -255,26 +268,43 @@ def get_grid(self, batchsize=1): return pos def forward(self, x, fx, T=None): - if self.unified_pos: - x = ( - self.pos.repeat(x.shape[0], 1, 1, 1) - .reshape(x.shape[0], self.H * self.W, self.ref * self.ref) - .to(x.device) - ) - if fx is not None: - fx = torch.cat((x, fx), -1) - fx = self.preprocess(fx) - else: - fx = self.preprocess(x) - fx = fx + self.placeholder[None, None, :] + with torch.autograd.profiler.record_function("prepeocess"): + if self.unified_pos: + # print(f"Applying unified pos, x shape: {x.shape}") + if self.pos.device != x.device: + # move the position tensor: + self.pos = self.pos.to(x.device) + + x = ( + self.pos.repeat(x.shape[0], 1, 1, 1) + .reshape(x.shape[0], self.H * self.W, self.ref * self.ref) + .to(x.device) + ) + # print(f"self.ref is {self.ref}") + # print(f"x shape after unified pos: {x.shape}") + if fx is not None: + # print(f"Applying fx, fx shape: {fx.shape}") + fx = torch.cat((x, fx), -1) + # print(f"fx shape after cat: {fx.shape}") + fx = self.preprocess(fx) + # print(f"fx shape after preprocess: {fx.shape}") + else: + # print(f"Applying just x, x shape: {x.shape}") + fx = self.preprocess(x) + # print(f"fx shape after preprocess: {fx.shape}") + fx = fx + self.placeholder[None, None, :] + # print(f"fx shape after placeholder: {fx.shape}") if T is not None: - Time_emb = timestep_embedding(T, self.n_hidden).repeat(1, x.shape[1], 1) - Time_emb = self.time_fc(Time_emb) - fx = fx + Time_emb + # print(f"Applying T, T shape: {T.shape}") + with torch.autograd.profiler.record_function("Time_Embedding"): + Time_emb = timestep_embedding(T, self.n_hidden).repeat(1, x.shape[1], 1) + Time_emb = self.time_fc(Time_emb) + fx = fx + Time_emb - for block in self.blocks: - fx = block(fx) + for i, block in enumerate(self.blocks): + with torch.autograd.profiler.record_function(f"Block_{i}"): + fx = block(fx) return fx @@ -396,6 +426,9 @@ def forward( The output tensor. """ + # print(f"Input x shape: {x.shape}") + # print(f"Input fx shape: {fx.shape if fx is not None else None}") + # print(f"Input T shape: {T.shape if T is not None else None}") y = self.model(x, fx, T) y = y.reshape(x.shape[0], self.H, self.W, -1) return y From bb770a35b3b59e1932e20f32e50be4bf192bff21 Mon Sep 17 00:00:00 2001 From: Corey Adams Date: Mon, 30 Jun 2025 13:06:25 -0700 Subject: [PATCH 02/23] Update Transolver to optionally enable transformer engine, and make the 'fix' version of the example easier to run, scalable, usable at full resolution, and faster --- examples/cfd/darcy_transolver/config_fix.yaml | 26 +- .../darcy_transolver/darcy_datapipe_fix.py | 137 ++--- .../train_transolver_darcy_fix.py | 484 +++++++++++------- .../cfd/darcy_transolver/validator_fix.py | 73 ++- .../models/transolver/Physics_Attention.py | 61 ++- physicsnemo/models/transolver/transolver.py | 89 ++-- 6 files changed, 544 insertions(+), 326 deletions(-) diff --git a/examples/cfd/darcy_transolver/config_fix.yaml b/examples/cfd/darcy_transolver/config_fix.yaml index dd97eeb8d7..7ac13ce133 100644 --- a/examples/cfd/darcy_transolver/config_fix.yaml +++ b/examples/cfd/darcy_transolver/config_fix.yaml @@ -20,6 +20,15 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +output_dir: ./output/darcy_transolver_fix +run_id: fp32_dev_test_fullres + +data: + train_path: /user_data/datasets/darcy_fix/example_data/piececonst_r421_N1024_smooth1.npz + test_path: /user_data/datasets/darcy_fix/example_data/piececonst_r421_N1024_smooth2.npz + resolution: 421 #421, 211, 141, 106, 85 all viable + batch_size: 8 # This is the GLOBAL batch size + model: space_dim: 2 n_layers: 8 @@ -31,12 +40,13 @@ model: mlp_ratio: 1 fun_dim: 1 out_dim: 1 - slice_dim: 32 + # slice_dim: 64 ref: 8 - unified_pos: 1 - slice_num: 64 + unified_pos: False + slice_num: 256 + transformer_engine: False - +precision: fp32 normaliser: permeability: @@ -53,12 +63,10 @@ scheduler: decay_pseudo_epochs: 8 training: - resolution: 85 - batch_size: 4 rec_results_freq : 100 - max_pseudo_epochs: 500 - pseudo_epoch_sample_size: 1000 + max_pseudo_epochs: 200 + pseudo_epoch_sample_size: 1024 validation: sample_size: 200 - validation_pseudo_epochs: 2 + validation_pseudo_epochs: 1 diff --git a/examples/cfd/darcy_transolver/darcy_datapipe_fix.py b/examples/cfd/darcy_transolver/darcy_datapipe_fix.py index be13843946..530248378d 100644 --- a/examples/cfd/darcy_transolver/darcy_datapipe_fix.py +++ b/examples/cfd/darcy_transolver/darcy_datapipe_fix.py @@ -14,33 +14,20 @@ # See the License for the specific language governing permissions and # limitations under the License. -import sys from dataclasses import dataclass from typing import Dict, Tuple, Union import numpy as np import torch -import warp as wp import scipy.io as scio +import torch.distributed as dist from physicsnemo.datapipes.datapipe import Datapipe from physicsnemo.datapipes.meta import DatapipeMetaData -from physicsnemo.datapipes.benchmarks.kernels.finite_difference import ( - darcy_mgrid_jacobi_iterative_batched_2d, - mgrid_inf_residual_batched_2d, -) -from physicsnemo.datapipes.benchmarks.kernels.initialization import ( - init_uniform_random_4d, -) -from physicsnemo.datapipes.benchmarks.kernels.utils import ( - bilinear_upsample_batched_2d, - fourier_to_array_batched_2d, - threshold_3d, -) Tensor = torch.Tensor -# TODO unsure if better to remove this. Keeping this in for now -wp.init() + +from physicsnemo.utils.profiling import profile class UnitTransformer: @@ -138,6 +125,7 @@ class Darcy2D_fix(Datapipe): Incompatable multi-grid and resolution settings """ + @profile def __init__( self, resolution: int = 256, @@ -148,13 +136,14 @@ def __init__( max_iterations: int = 30000, convergence_threshold: float = 1e-6, iterations_per_convergence_check: int = 1000, - nr_multigrids: int = 4, - normaliser: Union[Dict[str, Tuple[float, float]], None] = None, + # nr_multigrids: int = 4, + # normaliser: Union[Dict[str, Tuple[float, float]], None] = None, device: Union[str, torch.device] = "cuda", train_path: str = None, is_test: bool = False, x_normalizer: UnitTransformer = None, y_normalizer: UnitTransformer = None, + downsample: int = 5, ): super().__init__(meta=MetaData()) @@ -167,15 +156,15 @@ def __init__( self.max_iterations = max_iterations self.convergence_threshold = convergence_threshold self.iterations_per_convergence_check = iterations_per_convergence_check - self.nr_multigrids = nr_multigrids - self.normaliser = normaliser + # self.nr_multigrids = nr_multigrids + # self.normaliser = normaliser - # check normaliser keys - if self.normaliser is not None: - if not {"permeability", "darcy"}.issubset(set(self.normaliser.keys())): - raise ValueError( - "normaliser need to have keys permeability and darcy with mean and std" - ) + # # check normaliser keys + # if self.normaliser is not None: + # if not {"permeability", "darcy"}.issubset(set(self.normaliser.keys())): + # raise ValueError( + # "normaliser need to have keys permeability and darcy with mean and std" + # ) # Set up device for warp, warp has same naming convention as torch. if isinstance(device, torch.device): @@ -185,31 +174,22 @@ def __init__( # spatial dims self.dx = 1.0 / (self.resolution + 1) # pad edges by 1 for multi-grid self.dim = (self.batch_size, self.resolution + 1, self.resolution + 1) - self.fourier_dim = ( - 4, - self.batch_size, - self.nr_permeability_freq, - self.nr_permeability_freq, - ) # assert resolution is compatible with multi-grid method # if (resolution % 2 ** (nr_multigrids - 1)) != 0: # raise ValueError("Resolution is incompatible with number of sub grids.") - # allocate arrays for constructing dataset - self.darcy0 = wp.zeros(self.dim, dtype=float, device=self.device) - self.darcy1 = wp.zeros(self.dim, dtype=float, device=self.device) - self.permeability = wp.zeros(self.dim, dtype=float, device=self.device) - self.rand_fourier = wp.zeros(self.fourier_dim, dtype=float, device=self.device) - self.inf_residual = wp.zeros([1], dtype=float, device=self.device) self.train_path = train_path - self.downsample = 5 - self.r = self.downsample - self.h = int(((421 - 1) / self.r) + 1) - self.s = self.h - # print(f"=============={self.s}===============") + self.native_resolution = 421 # Native grid size + + # Calculate downsampling factor + if (self.native_resolution - 1) % (self.resolution - 1) != 0: + raise ValueError( + f"Resolution {self.resolution} is not achievable by strided sampling from native resolution {self.native_resolution}." + ) + self.r = (self.native_resolution - 1) // (self.resolution - 1) + self.s = self.resolution self.dx = 1.0 / self.s - # Output tenors self.output_k = None self.output_p = None @@ -217,10 +197,9 @@ def __init__( self.is_test = is_test if not self.is_test: - n_train = 1000 + self.n_train = 1024 else: - n_train = 200 - self.n_train = n_train + self.n_train = 200 if self.train_path is not None: self.__get_data__() @@ -233,28 +212,58 @@ def __init__( self.y_train = self.y_normalizer.encode(self.y_train) else: self.x_train = x_normalizer.encode(self.x_train) + self.y_train = y_normalizer.encode(self.y_train) + @profile def __get_normalizer__(self): return self.x_normalizer, self.y_normalizer + @profile def __get_data__(self): + + if self.train_path.endswith(".mat"): + data_dict = scio.loadmat(self.train_path) + elif self.train_path.endswith(".npz"): + data_dict = np.load(self.train_path) + + # Extract data from dicts: + self.x_train = data_dict["coeff"] + self.y_train = data_dict["sol"] + x = np.linspace(0, 1, self.s) y = np.linspace(0, 1, self.s) x, y = np.meshgrid(x, y) pos = np.c_[x.ravel(), y.ravel()] - pos = torch.tensor(pos, dtype=torch.float).unsqueeze(0).cuda() - self.x_train = scio.loadmat(self.train_path)["coeff"][ - : self.n_train, :: self.r, :: self.r - ][:, : self.s, : self.s] + pos = torch.tensor(pos, dtype=torch.float).cuda() + + # Downsampling logic + if self.r > 1: + # Downsample by slicing + self.x_train = self.x_train[: self.n_train, :: self.r, :: self.r][ + :, : self.s, : self.s + ] + self.y_train = self.y_train[: self.n_train, :: self.r, :: self.r][ + :, : self.s, : self.s + ] + else: + # No downsampling, use full resolution + self.x_train = self.x_train[: self.n_train, : self.s, : self.s] + self.y_train = self.y_train[: self.n_train, : self.s, : self.s] + + # Flatten them: self.x_train = self.x_train.reshape(self.n_train, -1) - self.x_train = torch.from_numpy(self.x_train).float().cuda() - self.y_train = scio.loadmat(self.train_path)["sol"][ - : self.n_train, :: self.r, :: self.r - ][:, : self.s, : self.s] self.y_train = self.y_train.reshape(self.n_train, -1) - self.y_train = torch.from_numpy(self.y_train).float().cuda() - self.pos_train = pos.repeat(self.n_train, 1, 1) + self.x_train = torch.from_numpy(self.x_train).float().cuda() + self.y_train = torch.from_numpy(self.y_train).float().cuda() + # Why are we repeating the postion? + # print(f"pos shape: {pos.shape}") + self.pos_train = pos + self.pos_train_batched = pos.repeat(self.batch_size, 1, 1).cuda() + # print(f"pos shape post repeat: {self.pos_train.shape}") + # self.pos_train = pos + + @profile def __iter__(self): """ Yields @@ -263,13 +272,17 @@ def __iter__(self): Infinite iterator that returns a batch of (permeability, darcy pressure) fields of size [batch, resolution, resolution] """ - # infinite generator + while True: - idx = np.random.choice(200, self.batch_size) + # Sample batch_size indices from this rank's shard + idx = np.random.choice(self.n_train, self.batch_size) + # All tensors are already on GPU, so no .cuda() needed x = self.x_train[idx] y = self.y_train[idx] - pos = self.pos_train[idx] - yield pos, x, y + yield self.pos_train_batched, x, y + + def __getitem__(self, idx): + return self.pos_train, self.x_train[idx], self.y_train[idx] def __len__(self): - return self.n_train // self.batch_size + return self.n_train diff --git a/examples/cfd/darcy_transolver/train_transolver_darcy_fix.py b/examples/cfd/darcy_transolver/train_transolver_darcy_fix.py index 76d2599d10..e218b67fed 100644 --- a/examples/cfd/darcy_transolver/train_transolver_darcy_fix.py +++ b/examples/cfd/darcy_transolver/train_transolver_darcy_fix.py @@ -14,68 +14,43 @@ # See the License for the specific language governing permissions and # limitations under the License. - +# Configuration imports: import hydra -from omegaconf import DictConfig -from math import ceil +from omegaconf import DictConfig, OmegaConf +import json -from torch.nn import MSELoss -from utils.testloss import TestLoss -from torch.optim import Adam, lr_scheduler, AdamW +from math import ceil -from physicsnemo.models.transolver import Transolver -from physicsnemo.distributed import DistributedManager -from physicsnemo.utils import StaticCaptureTraining, StaticCaptureEvaluateNoGrad -from physicsnemo.launch.utils import load_checkpoint, save_checkpoint -from physicsnemo.launch.logging import PythonLogger, LaunchLogger -from physicsnemo.launch.logging.mlflow import initialize_mlflow +# Base PyTorch imports: +import torch +import torch.distributed as dist -from darcy_datapipe_fix import Darcy2D_fix -from validator_fix import GridValidator +from torch.optim import lr_scheduler, AdamW +from torch.nn.parallel import DistributedDataParallel as DDP -class UnitTransformer: - """Unit transformer class for normalizing and denormalizing data.""" +# PyTorch Data tools +from torch.utils.data import DataLoader, DistributedSampler - def __init__(self, X): - self.mean = X.mean(dim=(0, 1), keepdim=True) - self.std = X.std(dim=(0, 1), keepdim=True) + 1e-8 +from torch.utils.tensorboard import SummaryWriter - def to(self, device): - self.mean = self.mean.to(device) - self.std = self.std.to(device) - return self +from utils.testloss import TestLoss - def cuda(self): - self.mean = self.mean.cuda() - self.std = self.std.cuda() +# Model imports from PhysicsNeMo +from physicsnemo.models.transolver import Transolver +from physicsnemo.distributed import DistributedManager - def cpu(self): - self.mean = self.mean.cpu() - self.std = self.std.cpu() +from physicsnemo.launch.utils import load_checkpoint, save_checkpoint +from physicsnemo.launch.logging import PythonLogger, RankZeroLoggingWrapper - def encode(self, x): - x = (x - self.mean) / (self.std) - return x +from darcy_datapipe_fix import Darcy2D_fix +from validator_fix import GridValidator - def decode(self, x): - return x * self.std + self.mean +from physicsnemo.utils.profiling import Profiler +from contextlib import nullcontext +import time # <-- Add this import for timing - def transform(self, X, inverse=True, component="all"): - if component == "all" or "all-reduce": - if inverse: - orig_shape = X.shape - return (X * (self.std - 1e-8) + self.mean).view(orig_shape) - else: - return (X - self.mean) / self.std - else: - if inverse: - orig_shape = X.shape - return ( - X * (self.std[:, component] - 1e-8) + self.mean[:, component] - ).view(orig_shape) - else: - return (X - self.mean[:, component]) / self.std[:, component] +prof = Profiler() def count_parameters(model): @@ -85,30 +60,127 @@ def count_parameters(model): continue params = parameter.numel() total_params += params - print(f"Total Trainable Params: {total_params}") return total_params +def forward_train_full_loop( + model, loss_fun, optimizer, pos, x, y, y_normalizer, context, scaler=None +): + """ + Forward and backward pass for one iteration, with optional mixed precision training. + Returns the loss for this minibatch + """ + with context: + pred = model(pos, fx=x.unsqueeze(-1)).squeeze(-1) + pred = y_normalizer.decode(pred) + loss = loss_fun(pred, y) + if scaler is not None: + scaler.scale(loss).backward() + scaler.step(optimizer) + scaler.update() + else: + loss.backward() + optimizer.step() + optimizer.zero_grad() + return loss + + +def train_epoch( + model, + optimizer, + scheduler, + train_dataloader, + loss_fun, + y_normalizer, + context, + scaler, +): + """ + One epoch of training. Returns the loss from the last minibatch used, averaged across replicas. + + """ + for i, batch in enumerate(train_dataloader): + pos, x, y = batch + loss = forward_train_full_loop( + model, loss_fun, optimizer, pos, x, y, y_normalizer, context, scaler + ) + scheduler.step() + + # At the end of the epoch, reduce the last local loss if needed: + dm = DistributedManager() + if dm.world_size > 1: + dist.all_reduce(loss.detach(), op=dist.ReduceOp.SUM) + loss = loss / dm.world_size + + return loss + + +def val_epoch(model, test_dataloader, loss_fun, y_normalizer): + """ + One epoch of validation. Returns the loss averaged across the entire validation set. + """ + val_loss = None + RL2 = None + for i, batch in enumerate(test_dataloader): + pos, x, y = batch + with torch.no_grad(): + pred = model(pos, fx=x.unsqueeze(-1)).squeeze(-1) + pred = y_normalizer.decode(pred) + loss = loss_fun(pred, y) + + # --- Relative L2 calculation --- + # print(f"pred shape: {pred.shape}") + # print(f"y shape: {y.shape}") + rel_l2 = torch.norm(pred.reshape(y.shape) - y) / torch.norm(y) + if RL2 is None: + RL2 = rel_l2 + else: + RL2 += rel_l2 + if val_loss is None: + val_loss = loss + else: + val_loss += loss + val_loss = val_loss / len(test_dataloader) + + dm = DistributedManager() + if dm.world_size > 1: + dist.all_reduce(val_loss, op=dist.ReduceOp.SUM) + dist.all_reduce(RL2, op=dist.ReduceOp.SUM) + val_loss = val_loss / dm.world_size + RL2 = RL2 / dm.world_size + return val_loss, pred, y, RL2 + + @hydra.main(version_base="1.3", config_path=".", config_name="config_fix.yaml") def darcy_trainer(cfg: DictConfig) -> None: """Training for the 2D Darcy flow benchmark problem.""" + ######################################################################## + # Initialize distributed tools + ######################################################################## DistributedManager.initialize() # Only call this once in the entire script! - dist = DistributedManager() # call if required elsewhere - - # initialize monitoring - log = PythonLogger(name="darcy_transolver") - log.file_logging() - initialize_mlflow( - experiment_name=f"Darcy_Transolver", - experiment_desc=f"training a Transformer-based PDE solver for the Darcy problem", - run_name=f"Darcy Transolver training", - run_desc=f"training Transolver for Darcy", - user_name="Haixu Wu, Huakun Luo, Haowen Wang", - mode="offline", - ) - LaunchLogger.initialize(use_mlflow=True) # PhysicsNeMo launch logger - - # define model, loss, optimiser, scheduler, data loader + dm = DistributedManager() # call if required elsewhere + + ######################################################################## + # Initialize monitoring and logging + ######################################################################## + logger = RankZeroLoggingWrapper(PythonLogger(name="darcy_transolver"), dm) + logger.file_logging() + + # === TensorBoard SummaryWriter === + # Only rank 0 writes logs to avoid duplication in DDP + writer = None + if dm.rank == 0: + log_dir = f"{cfg.output_dir}/runs/{cfg.run_id}" + writer = SummaryWriter(log_dir=log_dir) + + ######################################################################## + # Print the configuration to log + ######################################################################## + logger.info(json.dumps(OmegaConf.to_container(cfg), indent=4)) + + ######################################################################## + # define model + ######################################################################## model = Transolver( space_dim=cfg.model.space_dim, n_layers=cfg.model.n_layers, @@ -123,42 +195,73 @@ def darcy_trainer(cfg: DictConfig) -> None: slice_num=cfg.model.slice_num, ref=cfg.model.ref, unified_pos=cfg.model.unified_pos, - H=cfg.training.resolution, - W=cfg.training.resolution, - ).to(dist.device) - count_parameters(model) - loss_fun = TestLoss(size_average=False) + H=cfg.data.resolution, + W=cfg.data.resolution, + use_te=cfg.model.transformer_engine, + ).to(dm.device) + + if dm.world_size > 1: + model = DDP(model, device_ids=[dm.rank]) + + n_params = count_parameters(model) + logger.info(f"Total Trainable Params: {n_params}") + + ######################################################################## + # define loss and optimizer + ######################################################################## + loss_fun = TestLoss(size_average=True) optimizer = AdamW( model.parameters(), lr=cfg.scheduler.initial_lr, weight_decay=cfg.scheduler.weight_decay, ) - # scheduler = lr_scheduler.LambdaLR( - # optimizer, lr_lambda=lambda step: cfg.scheduler.decay_rate**step - # ) - - norm_vars = cfg.normaliser - normaliser = { - "permeability": (norm_vars.permeability.mean, norm_vars.permeability.std_dev), - "darcy": (norm_vars.darcy.mean, norm_vars.darcy.std_dev), - } - # train_dataloader = Darcy2D_fix( - # resolution=cfg.training.resolution, - # batch_size=cfg.training.batch_size, - # normaliser=normaliser, - # train_path="/data/fno/piececonst_r421_N1024_smooth1.mat", - # is_test=False, - # ) - train_dataloader = Darcy2D_fix( - resolution=cfg.training.resolution, - batch_size=cfg.training.batch_size, - normaliser=normaliser, - train_path="/data/fno/piececonst_r421_N1024_smooth1.mat", + + ######################################################################## + # Create the data pipes and samplers + ######################################################################## + + train_datapipe = Darcy2D_fix( + resolution=cfg.data.resolution, + batch_size=cfg.data.batch_size, + train_path=cfg.data.train_path, is_test=False, ) + # Sampler ensures disjoint instances on each rank + train_sampler = DistributedSampler( + train_datapipe, num_replicas=dm.world_size, rank=dm.rank, shuffle=True + ) + # DataLoader handles the batching + train_dataloader = DataLoader( + train_datapipe, + batch_size=cfg.data.batch_size // dm.world_size, + sampler=train_sampler, + drop_last=True, + ) + # Reuse the train normalizer for the test data: + # (The normalizer puts the inputs and targets to mean 0, std=1.0) + x_normalizer, y_normalizer = train_datapipe.__get_normalizer__() + + test_datapipe = Darcy2D_fix( + resolution=cfg.data.resolution, + batch_size=cfg.data.batch_size, + train_path=cfg.data.test_path, + is_test=True, + x_normalizer=x_normalizer, + y_normalizer=y_normalizer, + ) + test_sampler = DistributedSampler( + test_datapipe, num_replicas=dm.world_size, rank=dm.rank, shuffle=False + ) + test_dataloader = DataLoader( + test_datapipe, + batch_size=cfg.data.batch_size // dm.world_size, + sampler=test_sampler, + drop_last=True, + ) + # calculate steps per pseudo epoch steps_per_pseudo_epoch = ceil( - cfg.training.pseudo_epoch_sample_size / cfg.training.batch_size + cfg.training.pseudo_epoch_sample_size / cfg.data.batch_size ) scheduler = lr_scheduler.OneCycleLR( @@ -168,106 +271,145 @@ def darcy_trainer(cfg: DictConfig) -> None: epochs=cfg.training.max_pseudo_epochs, ) - x_normalizer, y_normalizer = train_dataloader.__get_normalizer__() - - test_dataloader = Darcy2D_fix( - resolution=cfg.training.resolution, - batch_size=cfg.training.batch_size, - normaliser=normaliser, - train_path="/data/fno/piececonst_r421_N1024_smooth2.mat", - is_test=True, - x_normalizer=x_normalizer, - ) - - validator = GridValidator(loss_fun=TestLoss(size_average=False), norm=y_normalizer) + validator = GridValidator(output_dir=f"{cfg.output_dir}/runs/{cfg.run_id}/plots") ckpt_args = { - "path": f"./checkpoints", + "path": f"{cfg.output_dir}/runs/{cfg.run_id}/checkpoints", "optimizer": optimizer, "scheduler": scheduler, "models": model, } - loaded_pseudo_epoch = load_checkpoint(device=dist.device, **ckpt_args) + loaded_pseudo_epoch = load_checkpoint(device=dm.device, **ckpt_args) - validation_iters = ceil(cfg.validation.sample_size / cfg.training.batch_size) - log_args = { - "name_space": "train", - "num_mini_batch": steps_per_pseudo_epoch, - "epoch_alert_freq": 1, - } - if cfg.training.pseudo_epoch_sample_size % cfg.training.batch_size != 0: - log.warning( + validation_iters = ceil(cfg.validation.sample_size / cfg.data.batch_size) + + if cfg.training.pseudo_epoch_sample_size % cfg.data.batch_size != 0: + logger.warning( f"increased pseudo_epoch_sample_size to multiple of \ - batch size: {steps_per_pseudo_epoch*cfg.training.batch_size}" + batch size: {steps_per_pseudo_epoch*cfg.data.batch_size}" ) - if cfg.validation.sample_size % cfg.training.batch_size != 0: - log.warning( + if cfg.validation.sample_size % cfg.data.batch_size != 0: + logger.warning( f"increased validation sample size to multiple of \ - batch size: {validation_iters*cfg.training.batch_size}" + batch size: {validation_iters*cfg.data.batch_size}" ) - # define forward passes for training and inference - @StaticCaptureTraining( - model=model, optim=optimizer, logger=log, use_amp=False, use_graphs=False - ) - def forward_train(pos, x, y, y_normalizer): - pred = model(pos, fx=x.unsqueeze(-1)).squeeze(-1) - pred = y_normalizer.decode(pred) - loss = loss_fun(pred, y) - return loss - - @StaticCaptureEvaluateNoGrad( - model=model, logger=log, use_amp=False, use_graphs=False - ) - def forward_eval(pos, x, y, y_normalizer): - pred = model(pos, fx=x.unsqueeze(-1)).squeeze(-1) - return y_normalizer.decode(pred) + # Initialize GradScaler for mixed precision training + if cfg.precision == "fp16": + context = torch.amp.autocast(device_type="cuda", dtype=torch.float16) + scaler = torch.amp.GradScaler("cuda") + elif cfg.precision == "bf16": + context = torch.amp.autocast(device_type="cuda", dtype=torch.bfloat16) + scaler = None + else: + context = nullcontext() + scaler = None if loaded_pseudo_epoch == 0: - log.success("Training started...") + logger.success("Training started...") else: - log.warning(f"Resuming training from pseudo epoch {loaded_pseudo_epoch+1}.") - - for pseudo_epoch in range( - max(1, loaded_pseudo_epoch + 1), cfg.training.max_pseudo_epochs + 1 - ): - # Wrap epoch in launch logger for console / MLFlow logs - with LaunchLogger(**log_args, epoch=pseudo_epoch) as logger: - for _, batch in zip(range(steps_per_pseudo_epoch), train_dataloader): - loss = forward_train(*batch, y_normalizer) - logger.log_minibatch({"loss": loss.detach() / cfg.training.batch_size}) - scheduler.step() - logger.log_epoch({"Learning Rate": optimizer.param_groups[0]["lr"]}) - - # save checkpoint - if pseudo_epoch % cfg.training.rec_results_freq == 0: - save_checkpoint(**ckpt_args, epoch=pseudo_epoch) - - # validation step - if pseudo_epoch % cfg.validation.validation_pseudo_epochs == 0: - with LaunchLogger("valid", epoch=pseudo_epoch) as logger: - total_loss = 0.0 - for _, batch in zip(range(validation_iters), test_dataloader): - val_loss = validator.compare( - batch[2], - forward_eval(*batch, y_normalizer), + logger.warning(f"Resuming training from pseudo epoch {loaded_pseudo_epoch+1}.") + + # Get the first batch of the test dataset for plotting + + with prof: + + for pseudo_epoch in range( + max(1, loaded_pseudo_epoch + 1), cfg.training.max_pseudo_epochs + 1 + ): + # --- TRAINING --- + train_start = time.time() + loss = train_epoch( + model, + optimizer, + scheduler, + train_dataloader, + loss_fun, + y_normalizer, + context, + scaler, + ) + train_time = time.time() - train_start + + # After training epoch, e.g. after loss, train_time, optimizer, etc. are available: + if torch.cuda.is_available(): + gpu_mem_reserved = torch.cuda.memory_reserved() / 1024**3 + else: + gpu_mem_reserved = 0 + + lr = optimizer.param_groups[0]["lr"] + + header = "mode\tEpoch\tloss\ttime\tLR\t\tGPU_mem" + values = f"train\t{pseudo_epoch}\t{loss.item():.4f}\t{train_time:.2f}\t{lr:.4e}\t{gpu_mem_reserved:.2f}" + + log_string = f"\n{header}\n{values}" + logger.info(log_string) + + # --- TensorBoard logging (only on rank 0) --- + if dm.rank == 0 and writer is not None: + # Images/sec/GPU: (num images processed in train_epoch) / train_time / num_gpus + # Each batch processes batch_size // world_size images, for steps_per_pseudo_epoch steps + images_per_epoch = len(train_dataloader) * ( + cfg.data.batch_size // dm.world_size + ) + images_per_sec_per_gpu = images_per_epoch / train_time + + writer.add_scalar("loss/train", loss.item(), pseudo_epoch) + writer.add_scalar("time_per_epoch/train", train_time, pseudo_epoch) + writer.add_scalar( + "images_per_sec_per_gpu/train", images_per_sec_per_gpu, pseudo_epoch + ) + writer.add_scalar("learning_rate/train", lr, pseudo_epoch) + + # save checkpoint + if pseudo_epoch % cfg.training.rec_results_freq == 0 and dm.rank == 0: + save_checkpoint(**ckpt_args, epoch=pseudo_epoch) + + # --- VALIDATION --- + if pseudo_epoch % cfg.validation.validation_pseudo_epochs == 0: + val_start = time.time() + val_loss, pred, y, RL2 = val_epoch( + model, test_dataloader, loss_fun, y_normalizer + ) + val_time = time.time() - val_start + + header = "mode\tEpoch\tloss\tRL2\ttime" + values = f"val\t{pseudo_epoch}\t{val_loss.item():.4f}\t{RL2.item():.4f}\t{val_time:.2f}" + + log_string = f"\n{header}\n{values}" + logger.info(log_string) + + # --- TensorBoard logging (only on rank 0) --- + if dm.rank == 0 and writer is not None: + # Validation images/sec/GPU + val_images = validation_iters * ( + cfg.data.batch_size // dm.world_size + ) + val_images_per_sec_per_gpu = val_images / val_time + writer.add_scalar("loss/val", val_loss.item(), pseudo_epoch) + writer.add_scalar("RL2/val", RL2.item(), pseudo_epoch) + writer.add_scalar("time_per_epoch/val", val_time, pseudo_epoch) + writer.add_scalar( + "images_per_sec_per_gpu/val", + val_images_per_sec_per_gpu, pseudo_epoch, - logger, ) - total_loss += val_loss - logger.log_epoch( - { - "Validation error": total_loss - / (validation_iters * cfg.training.batch_size) - } - ) + + if dm.rank == 0: + validator.make_plot(pred, y, pseudo_epoch, test_datapipe.s) # update learning rate # if pseudo_epoch % cfg.scheduler.decay_pseudo_epochs == 0: - save_checkpoint(**ckpt_args, epoch=cfg.training.max_pseudo_epochs) - log.success("Training completed *yay*") + if dm.rank == 0 and writer is not None: + writer.close() + logger.success("Training completed *yay*") if __name__ == "__main__": + # prof.enable("line_profile") + # prof.enable("torch") + # prof.initialize() darcy_trainer() + + # prof.finalize() diff --git a/examples/cfd/darcy_transolver/validator_fix.py b/examples/cfd/darcy_transolver/validator_fix.py index 658f9cfff4..2714179b8a 100644 --- a/examples/cfd/darcy_transolver/validator_fix.py +++ b/examples/cfd/darcy_transolver/validator_fix.py @@ -16,7 +16,8 @@ import matplotlib.pyplot as plt from torch import FloatTensor -from physicsnemo.launch.logging import LaunchLogger +import threading +import os class GridValidator: @@ -37,21 +38,50 @@ class GridValidator: def __init__( self, - loss_fun, - norm, font_size: float = 28.0, + output_dir: str = "./plots/", ): - self.norm = norm - self.criterion = loss_fun self.font_size = font_size self.headers = ("true", "prediction", "error") + self._plot_thread = None + self.output_dir = output_dir + os.makedirs(self.output_dir, exist_ok=True) - def compare( + def plot_figure( + self, target: FloatTensor, prediction: FloatTensor, step: int, resolution: int + ): + + target = target.cpu().numpy().reshape(-1, resolution, resolution)[0, :, :] + prediction = ( + prediction.reshape(-1, resolution, resolution) + .detach() + .cpu() + .numpy()[0, :, :] + ) + + plt.close("all") + plt.rcParams.update({"font.size": self.font_size}) + fig, ax = plt.subplots(1, 3, figsize=(15 * 3, 15), sharey=True) + im = [] + im.append(ax[0].imshow(target)) + im.append(ax[1].imshow(prediction)) + im.append(ax[2].imshow((prediction - target))) + + for ii in range(len(im)): + fig.colorbar(im[ii], ax=ax[ii], location="bottom", fraction=0.046, pad=0.04) + ax[ii].set_title(self.headers[ii]) + + plt.savefig(f"{self.output_dir}/validation_step_{step:03d}.png") + + def _plot_figure_thread(self, target, prediction, step, resolution): + self.plot_figure(target, prediction, step, resolution) + + def make_plot( self, prediction: FloatTensor, target: FloatTensor, step: int, - logger: LaunchLogger, + resolution: int, ) -> float: """compares model output, target and plots everything @@ -73,25 +103,16 @@ def compare( float validation error """ - loss = self.criterion(prediction, target) - # print(f"target.shape: {target.shape}, prediction.shape: {prediction.shape}") - print("logger begin") - target = target.cpu().numpy()[0, :, :] - prediction = prediction.reshape(-1, 85, 85).detach().cpu().numpy()[0, :, :] - - plt.close("all") - plt.rcParams.update({"font.size": self.font_size}) - fig, ax = plt.subplots(1, 3, figsize=(15 * 3, 15), sharey=True) - im = [] - im.append(ax[0].imshow(target)) - im.append(ax[1].imshow(prediction)) - im.append(ax[2].imshow((prediction - target))) - for ii in range(len(im)): - fig.colorbar(im[ii], ax=ax[ii], location="bottom", fraction=0.046, pad=0.04) - ax[ii].set_title(self.headers[ii]) + # Wait for previous plot thread if still running + if self._plot_thread is not None and self._plot_thread.is_alive(): + self._plot_thread.join() - logger.log_figure(figure=fig, artifact_file=f"validation_step_{step:03d}.png") - print("logger finished") + # Start new plot thread + self._plot_thread = threading.Thread( + target=self._plot_figure_thread, + args=(target, prediction, step, resolution), + ) + self._plot_thread.start() - return loss + return diff --git a/physicsnemo/models/transolver/Physics_Attention.py b/physicsnemo/models/transolver/Physics_Attention.py index c4b7807d7c..0a169a97f4 100644 --- a/physicsnemo/models/transolver/Physics_Attention.py +++ b/physicsnemo/models/transolver/Physics_Attention.py @@ -52,7 +52,7 @@ class Physics_Attention_Base(nn.Module): """ - def __init__(self, dim, heads, dim_head, dropout, slice_num): + def __init__(self, dim, heads, dim_head, dropout, slice_num, use_te): super().__init__() inner_dim = dim_head * heads self.dim_head = dim_head @@ -68,17 +68,22 @@ def __init__(self, dim, heads, dim_head, dropout, slice_num): for l_i in [self.in_project_slice]: torch.nn.init.orthogonal_(l_i.weight) # use a principled initialization - self.to_q = nn.Linear(dim_head, dim_head, bias=False) - self.to_k = nn.Linear(dim_head, dim_head, bias=False) - self.to_v = nn.Linear(dim_head, dim_head, bias=False) - - # # These are used in the transformer engine pass function: - # self.qkv_project = nn.Linear(dim_head, 3 * dim_head, bias=False) - # self.attn_fn = te.DotProductAttention(num_attention_heads=self.heads, - # kv_channels= self.dim_head, - # attention_dropout=dropout, - # qkv_format="bshd", - # softmax_scale=self.scale) + if not use_te: + self.to_q = nn.Linear(dim_head, dim_head, bias=False) + self.to_k = nn.Linear(dim_head, dim_head, bias=False) + self.to_v = nn.Linear(dim_head, dim_head, bias=False) + else: + # These are used in the transformer engine pass function: + self.qkv_project = nn.Linear(dim_head, 3 * dim_head, bias=False) + self.attn_fn = te.DotProductAttention( + num_attention_heads=self.heads, + kv_channels=self.dim_head, + attention_dropout=dropout, + qkv_format="bshd", + softmax_scale=self.scale, + ) + + self.use_te = use_te self.to_out = nn.Sequential(nn.Linear(inner_dim, dim), nn.Dropout(dropout)) @@ -114,12 +119,16 @@ def compute_slices_from_projections( - It then aggregates the latent features (fx) for each slice using these weights. - The aggregated features are normalized by the sum of weights for numerical stability. """ + # Project the latent space vectors on to the weight computation space, # and compute a temperature adjusted softmax. + clamped_temp = torch.clamp(self.temperature, min=0.1, max=5).to( + slice_projections.dtype + ) slice_weights = nn.functional.softmax( - slice_projections / torch.clamp(self.temperature, min=0.1, max=5), dim=-1 + slice_projections / clamped_temp, dim=-1 ) # [Batch, N_heads, N_tokens, Slice_num] - + slice_weights = slice_weights.to(slice_projections.dtype) # Average the slices over the token dimension slice_norm = slice_weights.sum(2) # [Batch, N_heads, Slice_num] @@ -146,7 +155,7 @@ def compute_slice_attention(self, slice_tokens: torch.Tensor) -> torch.Tensor: return out_slice_token - def compute_slice_attention2(self, slice_tokens: torch.Tensor) -> torch.Tensor: + def compute_slice_attention_te(self, slice_tokens: torch.Tensor) -> torch.Tensor: """ TE implementation of slice attention """ @@ -162,7 +171,7 @@ def compute_slice_attention2(self, slice_tokens: torch.Tensor) -> torch.Tensor: return out_slice_token2 - def compute_slice_attention3(self, slice_tokens: torch.Tensor) -> torch.Tensor: + def compute_slice_attention_sdpa(self, slice_tokens: torch.Tensor) -> torch.Tensor: """ Torch SDPA implementation of slice attention """ @@ -233,9 +242,11 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: # slice_tokens has shape [Batch, N_heads, N_tokens, head_dim] # Apply attention to the slice tokens - # out_slice_token = self.compute_slice_attention(slice_tokens) - # out_slice_token = self.compute_slice_attention2(slice_tokens) - out_slice_token = self.compute_slice_attention3(slice_tokens) + if self.use_te: + out_slice_token = self.compute_slice_attention_te(slice_tokens) + else: + out_slice_token = self.compute_slice_attention_sdpa(slice_tokens) + # Shape unchanged # Deslice: @@ -251,8 +262,10 @@ class Physics_Attention_Irregular_Mesh_2(Physics_Attention_Base): Specialization of PhysicsAttention to Irregular Meshes """ - def __init__(self, dim, heads=8, dim_head=64, dropout=0.0, slice_num=64): - super().__init__(dim, heads, dim_head, dropout, slice_num) + def __init__( + self, dim, heads=8, dim_head=64, dropout=0.0, slice_num=64, use_te=True + ): + super().__init__(dim, heads, dim_head, dropout, slice_num, use_te) inner_dim = dim_head * heads self.in_project_x = nn.Linear(dim, inner_dim) @@ -288,8 +301,9 @@ def __init__( H=101, W=31, kernel=3, + use_te=True, ): # kernel=3): - super().__init__(dim, heads, dim_head, dropout, slice_num) + super().__init__(dim, heads, dim_head, dropout, slice_num, use_te) inner_dim = dim_head * heads self.H = H @@ -341,8 +355,9 @@ def __init__( W=32, D=32, kernel=3, + use_te=True, ): - super().__init__(dim, heads, dim_head, dropout, slice_num) + super().__init__(dim, heads, dim_head, dropout, slice_num, use_te) inner_dim = dim_head * heads self.H = H diff --git a/physicsnemo/models/transolver/transolver.py b/physicsnemo/models/transolver/transolver.py index 875c31b350..be3f93341f 100644 --- a/physicsnemo/models/transolver/transolver.py +++ b/physicsnemo/models/transolver/transolver.py @@ -34,7 +34,13 @@ import numpy as np import torch import torch.nn as nn -import transformer_engine.pytorch as te + +try: + import transformer_engine.pytorch as te + + TE_AVAILABLE = True +except ImportError: + TE_AVAILABLE = False import physicsnemo # noqa: F401 for docs @@ -82,7 +88,6 @@ def __init__(self, n_input, n_hidden, n_output, n_layers=1, act="gelu", res=True ) def forward(self, x): - # print(x.shape) x = self.linear_pre(x) for i in range(self.n_layers): if self.res: @@ -108,10 +113,20 @@ def __init__( slice_num=32, H=85, W=85, + use_te=True, ): super().__init__() + + if use_te and not TE_AVAILABLE: + raise ImportError( + "Transformer Engine is not installed. Please install it with `pip install transformer-engine`." + ) + self.last_layer = last_layer - self.ln_1 = te.LayerNorm(hidden_dim) + if use_te: + self.ln_1 = te.LayerNorm(hidden_dim) + else: + self.ln_1 = nn.LayerNorm(hidden_dim) self.Attn = Physics_Attention_Structured_Mesh_2D( hidden_dim, heads=num_heads, @@ -120,28 +135,36 @@ def __init__( slice_num=slice_num, H=H, W=W, + use_te=use_te, ) - self.ln_mlp1 = te.LayerNormMLP( - hidden_size=hidden_dim, - ffn_hidden_size=hidden_dim * mlp_ratio, - ) - - # self.ln_2 = te.LayerNorm(hidden_dim) - # self.mlp = MLP( - # hidden_dim, - # hidden_dim * mlp_ratio, - # hidden_dim, - # n_layers=0, - # res=False, - # act=act, - # ) - if self.last_layer: - self.ln_mlp2 = te.LayerNormLinear( - in_features=hidden_dim, out_features=out_dim + if use_te: + self.ln_mlp1 = te.LayerNormMLP( + hidden_size=hidden_dim, + ffn_hidden_size=hidden_dim * mlp_ratio, ) - # self.ln_3 = te.LayerNorm(hidden_dim) - # self.mlp2 = nn.Linear(hidden_dim, out_dim) + else: + self.ln_mlp1 = nn.Sequential( + nn.LayerNorm(hidden_dim), + MLP( + hidden_dim, + hidden_dim * mlp_ratio, + hidden_dim, + n_layers=0, + res=False, + act=act, + ), + ) + if self.last_layer: + if use_te: + self.ln_mlp2 = te.LayerNormLinear( + in_features=hidden_dim, out_features=out_dim + ) + else: + self.ln_mlp2 = nn.Sequential( + nn.LayerNorm(hidden_dim), + nn.Linear(hidden_dim, out_dim), + ) def forward(self, fx): fx = self.Attn(self.ln_1(fx)) + fx @@ -172,6 +195,7 @@ def __init__( unified_pos=False, H=85, W=85, + use_te=True, ): super().__init__() self.__name__ = "Transolver_2D" @@ -219,14 +243,15 @@ def __init__( H=H, W=W, last_layer=(_ == n_layers - 1), + use_te=use_te, ) for _ in range(n_layers) ] ) self.initialize_weights() - self.placeholder = nn.Parameter( - (1 / (n_hidden)) * torch.rand(n_hidden, dtype=torch.float) - ) + # self.placeholder = nn.Parameter( + # (1 / (n_hidden)) * torch.rand(n_hidden, dtype=torch.float) + # ) def initialize_weights(self): self.apply(self._init_weights) @@ -270,7 +295,6 @@ def get_grid(self, batchsize=1): def forward(self, x, fx, T=None): with torch.autograd.profiler.record_function("prepeocess"): if self.unified_pos: - # print(f"Applying unified pos, x shape: {x.shape}") if self.pos.device != x.device: # move the position tensor: self.pos = self.pos.to(x.device) @@ -280,23 +304,14 @@ def forward(self, x, fx, T=None): .reshape(x.shape[0], self.H * self.W, self.ref * self.ref) .to(x.device) ) - # print(f"self.ref is {self.ref}") - # print(f"x shape after unified pos: {x.shape}") if fx is not None: - # print(f"Applying fx, fx shape: {fx.shape}") fx = torch.cat((x, fx), -1) - # print(f"fx shape after cat: {fx.shape}") fx = self.preprocess(fx) - # print(f"fx shape after preprocess: {fx.shape}") else: - # print(f"Applying just x, x shape: {x.shape}") fx = self.preprocess(x) - # print(f"fx shape after preprocess: {fx.shape}") fx = fx + self.placeholder[None, None, :] - # print(f"fx shape after placeholder: {fx.shape}") if T is not None: - # print(f"Applying T, T shape: {T.shape}") with torch.autograd.profiler.record_function("Time_Embedding"): Time_emb = timestep_embedding(T, self.n_hidden).repeat(1, x.shape[1], 1) Time_emb = self.time_fc(Time_emb) @@ -365,6 +380,8 @@ class Transolver(Module): The height of the mesh. W : int The width of the mesh. + use_te: bool + Whether to use transformer engine backend when possible. """ def __init__( @@ -384,6 +401,7 @@ def __init__( unified_pos: bool, H: int, W: int, + use_te: bool = True, ) -> None: super().__init__(meta=MetaData()) self.H = H @@ -404,6 +422,7 @@ def __init__( unified_pos=unified_pos, H=H, W=W, + use_te=use_te, ) def forward( From 3d392c379d0eca735ebdead6c2bfdc3e020b79e5 Mon Sep 17 00:00:00 2001 From: Corey Adams Date: Tue, 1 Jul 2025 10:01:36 -0700 Subject: [PATCH 03/23] Clean up transolver architecture and code to enable irregular and 3D meshes. Enhance documentation. --- examples/cfd/darcy_transolver/config_fix.yaml | 15 +- .../train_transolver_darcy_fix.py | 18 +- .../models/transolver/Physics_Attention.py | 64 +- physicsnemo/models/transolver/transolver.py | 752 +++++++++++++----- 4 files changed, 608 insertions(+), 241 deletions(-) diff --git a/examples/cfd/darcy_transolver/config_fix.yaml b/examples/cfd/darcy_transolver/config_fix.yaml index 7ac13ce133..ce24362bfb 100644 --- a/examples/cfd/darcy_transolver/config_fix.yaml +++ b/examples/cfd/darcy_transolver/config_fix.yaml @@ -21,17 +21,17 @@ # SOFTWARE. output_dir: ./output/darcy_transolver_fix -run_id: fp32_dev_test_fullres +run_id: fp32_dev_fullres_b64_s64-te data: train_path: /user_data/datasets/darcy_fix/example_data/piececonst_r421_N1024_smooth1.npz test_path: /user_data/datasets/darcy_fix/example_data/piececonst_r421_N1024_smooth2.npz resolution: 421 #421, 211, 141, 106, 85 all viable - batch_size: 8 # This is the GLOBAL batch size + batch_size: 32 # This is the GLOBAL batch size model: space_dim: 2 - n_layers: 8 + n_layers: 4 n_hidden: 128 dropout: 0.0 n_head: 8 @@ -40,11 +40,10 @@ model: mlp_ratio: 1 fun_dim: 1 out_dim: 1 - # slice_dim: 64 ref: 8 unified_pos: False - slice_num: 256 - transformer_engine: False + slice_num: 64 + transformer_engine: True precision: fp32 @@ -58,13 +57,13 @@ normaliser: scheduler: initial_lr: 1.E-3 - decay_rate: 1.E-5 + decay_rate: 5.E-5 weight_decay: 1.E-5 decay_pseudo_epochs: 8 training: rec_results_freq : 100 - max_pseudo_epochs: 200 + max_pseudo_epochs: 1000 pseudo_epoch_sample_size: 1024 validation: diff --git a/examples/cfd/darcy_transolver/train_transolver_darcy_fix.py b/examples/cfd/darcy_transolver/train_transolver_darcy_fix.py index e218b67fed..2eea776208 100644 --- a/examples/cfd/darcy_transolver/train_transolver_darcy_fix.py +++ b/examples/cfd/darcy_transolver/train_transolver_darcy_fix.py @@ -70,6 +70,7 @@ def forward_train_full_loop( Forward and backward pass for one iteration, with optional mixed precision training. Returns the loss for this minibatch """ + dm = DistributedManager() with context: pred = model(pos, fx=x.unsqueeze(-1)).squeeze(-1) pred = y_normalizer.decode(pred) @@ -128,19 +129,24 @@ def val_epoch(model, test_dataloader, loss_fun, y_normalizer): pred = y_normalizer.decode(pred) loss = loss_fun(pred, y) - # --- Relative L2 calculation --- - # print(f"pred shape: {pred.shape}") - # print(f"y shape: {y.shape}") - rel_l2 = torch.norm(pred.reshape(y.shape) - y) / torch.norm(y) + # Compute per-sample relative L2 error + diff = pred.reshape(y.shape) - y + rel_l2 = torch.norm(diff.view(diff.shape[0], -1), dim=1) / torch.norm( + y.view(y.shape[0], -1), dim=1 + ) + rel_l2_mean = rel_l2.mean() + if RL2 is None: - RL2 = rel_l2 + RL2 = rel_l2_mean else: - RL2 += rel_l2 + RL2 += rel_l2_mean if val_loss is None: val_loss = loss else: val_loss += loss + val_loss = val_loss / len(test_dataloader) + RL2 = RL2 / len(test_dataloader) dm = DistributedManager() if dm.world_size > 1: diff --git a/physicsnemo/models/transolver/Physics_Attention.py b/physicsnemo/models/transolver/Physics_Attention.py index 0a169a97f4..c303634391 100644 --- a/physicsnemo/models/transolver/Physics_Attention.py +++ b/physicsnemo/models/transolver/Physics_Attention.py @@ -122,21 +122,23 @@ def compute_slices_from_projections( # Project the latent space vectors on to the weight computation space, # and compute a temperature adjusted softmax. - clamped_temp = torch.clamp(self.temperature, min=0.1, max=5).to( + clamped_temp = torch.clamp(self.temperature, min=0.5, max=5).to( slice_projections.dtype ) slice_weights = nn.functional.softmax( slice_projections / clamped_temp, dim=-1 ) # [Batch, N_heads, N_tokens, Slice_num] + + # Cast to the computation type (since the parameter is probably fp32) slice_weights = slice_weights.to(slice_projections.dtype) # Average the slices over the token dimension slice_norm = slice_weights.sum(2) # [Batch, N_heads, Slice_num] - # This does the projection of the latent space fx by the weights: - slice_token = torch.matmul(slice_weights.transpose(2, 3), fx) - # Apply the normalization (summed weights) - slice_token = slice_token / ((slice_norm[:, :, :, None] + 1e-5)) # B H G D + # Computing the slice tokens is a matmul followed by a normalization. + # It can, unfortunately, overflow in reduced precision, so normalize first: + slice_weights = slice_weights / (slice_norm[:, :, None, :] + 1e-2) + slice_token = torch.matmul(slice_weights.transpose(2, 3), fx) return slice_weights, slice_token @@ -226,32 +228,61 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: # Project the inputs onto learned spaces: x_mid, fx_mid = self.project_input_onto_slices(x) + # if (x_mid.isnan().any()): + # print(f"Rank {dm.rank} x_mid is nan") + # exit() + # if (fx_mid.isnan().any()): + # print(f"Rank {dm.rank} fx_mid is nan") + # exit() # x_mid and fx_mid should have shapes of [B, N_head, N_tokens, Head_dim] # Perform the linear projection of learned latent space onto slices: slice_projections = self.in_project_slice(x_mid) - + # if (slice_projections.isnan().any()): + # print(f"Rank {dm.rank} slice_projections is nan") + # exit() # Slice projections has shape [B, N_head, N_tokens, Head_dim], but head_dim may have changed! # Use the slice projections and learned spaces to compute the slices, and their weights: slice_weights, slice_tokens = self.compute_slices_from_projections( slice_projections, fx_mid ) - + # if (slice_weights.isnan().any()): + # print(f"Rank {dm.rank} slice_weights is nan") + # exit() + # if (slice_tokens.isnan().any()) or (slice_tokens.isinf().any()): + # print(f"Rank {dm.rank} slice_tokens is nan or inf") + # torch.save(slice_tokens, f"slice_tokens_{dm.rank}.pt") + # torch.save(slice_weights, f"slice_weights_{dm.rank}.pt") + # torch.save(slice_projections, f"slice_projections_{dm.rank}.pt") + # torch.save(fx_mid, f"fx_mid_{dm.rank}.pt") + # exit() # slice_weights has shape [Batch, N_heads, N_tokens, Slice_num] # slice_tokens has shape [Batch, N_heads, N_tokens, head_dim] + # print(f"Rank {dm.rank} Min/max/std of slice tokens: {slice_tokens.min()}, {slice_tokens.max()}, {slice_tokens.std()}") + # Apply attention to the slice tokens if self.use_te: out_slice_token = self.compute_slice_attention_te(slice_tokens) + # out_slice_token = self.compute_slice_attention(slice_tokens) else: out_slice_token = self.compute_slice_attention_sdpa(slice_tokens) + # if (out_slice_token.isnan().any()): + # print(f"Rank {dm.rank} out_slice_token is nan") + # torch.save(slice_tokens, f"slice_tokens_{dm.rank}.pt") + # torch.save(slice_weights, f"slice_weights_{dm.rank}.pt") + # torch.save(slice_projections, f"slice_projections_{dm.rank}.pt") + # torch.save(fx_mid, f"fx_mid_{dm.rank}.pt") + # exit() # Shape unchanged # Deslice: outputs = self.project_attention_outputs(out_slice_token, slice_weights) - + # if (outputs.isnan().any()): + # print(f"Rank {dm.rank} outputs is nan") + # exit() # Outputs now has the same shape as the original input x return outputs @@ -294,20 +325,19 @@ class Physics_Attention_Structured_Mesh_2D_2(Physics_Attention_Base): def __init__( self, dim, + spatial_shape, heads=8, dim_head=64, dropout=0.0, slice_num=64, - H=101, - W=31, kernel=3, use_te=True, ): # kernel=3): super().__init__(dim, heads, dim_head, dropout, slice_num, use_te) inner_dim = dim_head * heads - self.H = H - self.W = W + self.H = spatial_shape[0] + self.W = spatial_shape[1] self.in_project_x = nn.Conv2d(dim, inner_dim, kernel, 1, kernel // 2) self.in_project_fx = nn.Conv2d(dim, inner_dim, kernel, 1, kernel // 2) @@ -347,22 +377,20 @@ class Physics_Attention_Structured_Mesh_3D_2(Physics_Attention_Base): def __init__( self, dim, + spatial_shape, heads=8, dim_head=64, dropout=0.0, slice_num=32, - H=32, - W=32, - D=32, kernel=3, use_te=True, ): super().__init__(dim, heads, dim_head, dropout, slice_num, use_te) inner_dim = dim_head * heads - self.H = H - self.W = W - self.D = D + self.H = spatial_shape[0] + self.W = spatial_shape[1] + self.D = spatial_shape[2] self.in_project_x = nn.Conv3d(dim, inner_dim, kernel, 1, kernel // 2) self.in_project_fx = nn.Conv3d(dim, inner_dim, kernel, 1, kernel // 2) diff --git a/physicsnemo/models/transolver/transolver.py b/physicsnemo/models/transolver/transolver.py index be3f93341f..eaa2c1f49a 100644 --- a/physicsnemo/models/transolver/transolver.py +++ b/physicsnemo/models/transolver/transolver.py @@ -49,9 +49,15 @@ from .Embedding import timestep_embedding # from .Physics_Attention import Physics_Attention_Structured_Mesh_2D +from .Physics_Attention import ( + Physics_Attention_Irregular_Mesh_2 as Physics_Attention_Irregular_Mesh, +) from .Physics_Attention import ( Physics_Attention_Structured_Mesh_2D_2 as Physics_Attention_Structured_Mesh_2D, ) +from .Physics_Attention import ( + Physics_Attention_Structured_Mesh_3D_2 as Physics_Attention_Structured_Mesh_3D, +) ACTIVATION = { "gelu": nn.GELU, @@ -99,7 +105,7 @@ def forward(self, x): class Transolver_block(nn.Module): - """Transformer encoder block.""" + """Transformer encoder block, replacing standard attention with physics attention.""" def __init__( self, @@ -111,8 +117,7 @@ def __init__( last_layer=False, out_dim=1, slice_num=32, - H=85, - W=85, + spatial_shape: tuple[int] | None = None, use_te=True, ): super().__init__() @@ -127,16 +132,41 @@ def __init__( self.ln_1 = te.LayerNorm(hidden_dim) else: self.ln_1 = nn.LayerNorm(hidden_dim) - self.Attn = Physics_Attention_Structured_Mesh_2D( - hidden_dim, - heads=num_heads, - dim_head=hidden_dim // num_heads, - dropout=dropout, - slice_num=slice_num, - H=H, - W=W, - use_te=use_te, - ) + + if spatial_shape is None: + self.Attn = Physics_Attention_Irregular_Mesh( + hidden_dim, + heads=num_heads, + dim_head=hidden_dim // num_heads, + dropout=dropout, + slice_num=slice_num, + use_te=use_te, + ) + else: + if len(spatial_shape) == 2: + self.Attn = Physics_Attention_Structured_Mesh_2D( + hidden_dim, + spatial_shape=spatial_shape, + heads=num_heads, + dim_head=hidden_dim // num_heads, + dropout=dropout, + slice_num=slice_num, + use_te=use_te, + ) + elif len(spatial_shape) == 3: + self.Attn = Physics_Attention_Structured_Mesh_3D( + hidden_dim, + spatial_shape=spatial_shape, + heads=num_heads, + dim_head=hidden_dim // num_heads, + dropout=dropout, + slice_num=slice_num, + use_te=use_te, + ) + else: + raise Exception( + f"Unexpected length of spatial shape encountered in Transolver_block: {len(spatial_shape)}" + ) if use_te: self.ln_mlp1 = te.LayerNormMLP( @@ -169,67 +199,338 @@ def __init__( def forward(self, fx): fx = self.Attn(self.ln_1(fx)) + fx fx = self.ln_mlp1(fx) + fx - # fx = self.mlp(self.ln_2(fx)) + fx if self.last_layer: return self.ln_mlp2(fx) - # return self.mlp2(self.ln_3(fx)) else: return fx -class Model(nn.Module): +# class Model(nn.Module): +# def __init__( +# self, +# space_dim=1, +# n_layers=5, +# n_hidden=256, +# dropout=0.0, +# n_head=8, +# Time_Input=False, +# act="gelu", +# mlp_ratio=1, +# fun_dim=1, +# out_dim=1, +# slice_num=32, +# ref=8, +# unified_pos=False, +# H=85, +# W=85, +# use_te=True, +# ): +# super().__init__() +# self.__name__ = "Transolver_2D" +# self.H = H +# self.W = W +# self.ref = ref +# self.unified_pos = unified_pos +# if self.unified_pos: +# self.pos = self.get_grid() +# self.preprocess = MLP( +# fun_dim + self.ref * self.ref, +# n_hidden * 2, +# n_hidden, +# n_layers=0, +# res=False, +# act=act, +# ) +# else: +# self.preprocess = MLP( +# fun_dim + space_dim, +# n_hidden * 2, +# n_hidden, +# n_layers=0, +# res=False, +# act=act, +# ) + +# self.Time_Input = Time_Input +# self.n_hidden = n_hidden +# self.space_dim = space_dim +# if Time_Input: +# self.time_fc = nn.Sequential( +# nn.Linear(n_hidden, n_hidden), nn.SiLU(), nn.Linear(n_hidden, n_hidden) +# ) +# self.blocks = nn.ModuleList( +# [ +# Transolver_block( +# num_heads=n_head, +# hidden_dim=n_hidden, +# dropout=dropout, +# act=act, +# mlp_ratio=mlp_ratio, +# out_dim=out_dim, +# slice_num=slice_num, +# H=H, +# W=W, +# last_layer=(_ == n_layers - 1), +# use_te=use_te, +# ) +# for _ in range(n_layers) +# ] +# ) +# self.initialize_weights() +# # self.placeholder = nn.Parameter( +# # (1 / (n_hidden)) * torch.rand(n_hidden, dtype=torch.float) +# # ) + +# def initialize_weights(self): +# self.apply(self._init_weights) + +# def _init_weights(self, m): +# if isinstance(m, nn.Linear): +# nn.init.trunc_normal_(m.weight, std=0.02) +# if isinstance(m, nn.Linear) and m.bias is not None: +# nn.init.constant_(m.bias, 0) +# elif isinstance(m, (nn.LayerNorm, te.LayerNorm, nn.BatchNorm1d)): +# nn.init.constant_(m.bias, 0) +# nn.init.constant_(m.weight, 1.0) + +# def get_grid(self, batchsize=1): +# size_x, size_y = self.H, self.W +# gridx = torch.tensor(np.linspace(0, 1, size_x), dtype=torch.float) +# gridx = gridx.reshape(1, size_x, 1, 1).repeat([batchsize, 1, size_y, 1]) +# gridy = torch.tensor(np.linspace(0, 1, size_y), dtype=torch.float) +# gridy = gridy.reshape(1, 1, size_y, 1).repeat([batchsize, size_x, 1, 1]) +# grid = torch.cat((gridx, gridy), dim=-1) # B H W 2 + +# gridx = torch.tensor(np.linspace(0, 1, self.ref), dtype=torch.float) +# gridx = gridx.reshape(1, self.ref, 1, 1).repeat([batchsize, 1, self.ref, 1]) +# gridy = torch.tensor(np.linspace(0, 1, self.ref), dtype=torch.float) +# gridy = gridy.reshape(1, 1, self.ref, 1).repeat([batchsize, self.ref, 1, 1]) +# grid_ref = torch.cat((gridx, gridy), dim=-1) # B H W 8 8 2 + +# pos = ( +# torch.sqrt( +# torch.sum( +# (grid[:, :, :, None, None, :] - grid_ref[:, None, None, :, :, :]) +# ** 2, +# dim=-1, +# ) +# ) +# .reshape(batchsize, size_x, size_y, self.ref * self.ref) +# .contiguous() +# ) +# return pos + +# def forward(self, x, fx, T=None): +# with torch.autograd.profiler.record_function("prepeocess"): +# if self.unified_pos: +# if self.pos.device != x.device: +# # move the position tensor: +# self.pos = self.pos.to(x.device) + +# x = ( +# self.pos.repeat(x.shape[0], 1, 1, 1) +# .reshape(x.shape[0], self.H * self.W, self.ref * self.ref) +# .to(x.device) +# ) +# if fx is not None: +# fx = torch.cat((x, fx), -1) +# fx = self.preprocess(fx) +# else: +# fx = self.preprocess(x) +# fx = fx + self.placeholder[None, None, :] + +# if T is not None: +# with torch.autograd.profiler.record_function("Time_Embedding"): +# Time_emb = timestep_embedding(T, self.n_hidden).repeat(1, x.shape[1], 1) +# Time_emb = self.time_fc(Time_emb) +# fx = fx + Time_emb + +# for i, block in enumerate(self.blocks): +# with torch.autograd.profiler.record_function(f"Block_{i}"): +# fx = block(fx) + +# return fx + + +@dataclass +class MetaData(ModelMetaData): + name: str = "Transolver" + # Optimization + jit: bool = False + cuda_graphs: bool = False + amp: bool = False + # Inference + onnx_cpu: bool = False # No FFT op on CPU + onnx_gpu: bool = True + onnx_runtime: bool = True + # Physics informed + var_dim: int = 1 + func_torch: bool = False + auto_grad: bool = False + + +class Transolver(Module): + """ + Transolver model, adapted from original transolver code. + + Transolver is an adaptation of the transformer architecture, with a physics-attention + mechanism replacing the standard attention mechanism. + + For more architecture details, see: https://arxiv.org/pdf/2402.02366 and https://arxiv.org/pdf/2502.02414 + + Transolver can work on structured or unstructured data points as a model construction choice: + - unstructured data (like a mesh) should provide some sort of positional encoding to accompany inputs + - structured data (2D and 3D grids) can provide positional encodings optionally + + When constructing Transolver, you can choose to use "unified position" or not. If you select "unified + position" (`unified_pos=True`), then + + If using structured data, pass the structured shape as a tuple in the model constructor. + Length 2 tuples are assumed to be image-like, length 3 tuples are assumed to be 3D voxel like. + Other structured shape sizes are not supported. Passing a structured_shape of None assumes irregular data. + + Output shape will have the same spatial shape as the input shape, with potentially more features + + Also can support Transolver++ implementation. When using the distributed algorithm + of Transolver++, use PhysicsNeMo's ShardTensor implementation to support automatic + domain parallelism and 2D parallelization (data parallel + domain parallel, for example). + + Note + ---- + + + Parameters + ---------- + functional_dim : int + The dimension of the input values, not including any embeddings. No Default. + Input will be concatenated with embeddings or unified position before processing + with PhysicsAttention blocks. Originally known as "fun_dim" + out_dim : int + The dimension of the output of the model. This is a mandatory parameter. + embedding_dim : int | None + The spatial dimension of the input data embeddings. Should include not just + position but all computed embedding features. Default is None, but if + `unified_pos=False` this is a mandatory parameter. Originally named "space_dim" + n_layers : int + The number of transformer PhysicsAttention layers in the model. Default of 4. + n_hidden : int + The hidden dimension of the transformer. Default of 256. Projection is made + from the input data + embeddings in the early preprocessing, before the + PhysicsAttention layers. + dropout : float + The dropout rate, applied across the PhysicsAttention Layers. Default is 0.0 + n_head : int + The number of attention heads in each PhysicsAttention Layer. Default is 8. Note + that the number of heads must evenly divide the `n_hidden` parameter to yield an + integer head dimension. + act : str + The activation function, default is Gelu. + mlp_ratio : int + The ratio of hidden dimension in the MLP, default is 4. Used in the MLPs in the + PhysicsAttention Layers. + slice_num : int + The number of slices in the PhysicsAttention layers. Default is 32. Represents the + number of learned states each layer should project inputs onto. + unified_pos : bool + Whether to use unified positional embeddings. Unified positions are only available for + structured data (2D grids, 3D grids). They are computed once initially, and reused through + training in place of embeddings. + ref : int + The reference dimension size when using unified positions. Default is 8. Will be + used to create a linear grid in spatial dimensions to serve as spatial embeddings. + If `unified_pos=False`, this value is unused. + structured_shape : None | tuple(int) + The shape of the latent space. If None, assumes irregular latent space. If not + `None`, this parameter can only be a length-2 or length-3 tuple of ints. + use_te: bool + Whether to use transformer engine backend when possible. + Time_Input : bool + Whether to include time embeddings. Default is false + """ + def __init__( self, - space_dim=1, - n_layers=5, - n_hidden=256, - dropout=0.0, - n_head=8, - Time_Input=False, - act="gelu", - mlp_ratio=1, - fun_dim=1, - out_dim=1, - slice_num=32, - ref=8, - unified_pos=False, - H=85, - W=85, - use_te=True, - ): - super().__init__() - self.__name__ = "Transolver_2D" - self.H = H - self.W = W - self.ref = ref - self.unified_pos = unified_pos - if self.unified_pos: - self.pos = self.get_grid() - self.preprocess = MLP( - fun_dim + self.ref * self.ref, - n_hidden * 2, - n_hidden, - n_layers=0, - res=False, - act=act, + functional_dim: int, + out_dim: int, + embedding_dim: int | None = None, + n_layers: int = 4, + n_hidden: int = 256, + dropout: float = 0.0, + n_head: int = 8, + act: str = "gelu", + mlp_ratio: int = 4, + slice_num: int = 32, + unified_pos: bool = False, + ref: int = 8, + structured_shape: None | tuple[int] = None, + use_te: bool = True, + Time_Input: bool = False, + ) -> None: + super().__init__(meta=MetaData()) + self.__name__ = "Transolver" + + # Check that the hidden dimension and head dimensions are compatible: + if not n_hidden % n_head == 0: + raise ValueError( + f"Transolver requires n_hidden % n_head == 0, but instead got {n_hidden % n_head}" ) + + # Check the shape of the data, if it's structured data: + if structured_shape is not None: + # Has to be 2D or 3D data: + if len(structured_shape) not in [2, 3]: + raise ValueError( + f"Transolver can only use structured data in 2D or 3D, got {structured_shape}" + ) + + # Ensure it's all integers > 0: + if not all([s > 0 and s == int(s) for s in structured_shape]): + raise ValueError( + f"Transolver can only use integer shapes > 0, got {structured_shape}" + ) else: - self.preprocess = MLP( - fun_dim + space_dim, - n_hidden * 2, - n_hidden, - n_layers=0, - res=False, - act=act, - ) + # It's mandatory for unified position: + if unified_pos: + raise ValueError( + "Transolver requires structured_shape to be passed if using unified_pos=True" + ) + + self.structured_shape = structured_shape + + # If we're using the unified position, create and save the position embeddings: + + if unified_pos: + if structured_shape is None: + raise ValueError( + "Transolver can not use unified position without a structured_shape argument (got None)" + ) + + # This ensures embedding is tracked by torch and moves to the GPU, and saves/loads + self.register_buffer("embedding", self.get_grid(ref)) + self.embedding_dim = ref * ref + mlp_input_dimension = functional_dim + ref * ref + + else: + self.embedding_dim = embedding_dim + mlp_input_dimension = functional_dim + embedding_dim + + # This MLP is the initial projection onto the hidden space + self.preprocess = MLP( + mlp_input_dimension, + n_hidden * 2, + n_hidden, + n_layers=0, + res=False, + act=act, + ) self.Time_Input = Time_Input self.n_hidden = n_hidden - self.space_dim = space_dim if Time_Input: self.time_fc = nn.Sequential( nn.Linear(n_hidden, n_hidden), nn.SiLU(), nn.Linear(n_hidden, n_hidden) ) + self.blocks = nn.ModuleList( [ Transolver_block( @@ -240,8 +541,7 @@ def __init__( mlp_ratio=mlp_ratio, out_dim=out_dim, slice_num=slice_num, - H=H, - W=W, + spatial_shape=structured_shape, last_layer=(_ == n_layers - 1), use_te=use_te, ) @@ -265,18 +565,34 @@ def _init_weights(self, m): nn.init.constant_(m.bias, 0) nn.init.constant_(m.weight, 1.0) - def get_grid(self, batchsize=1): - size_x, size_y = self.H, self.W + def get_grid(self, ref: int, batchsize: int = 1) -> torch.Tensor: + """ + Generate a unified positional encoding grid for structured 2D data. + + Parameters + ---------- + ref : int + The reference grid size for the unified position encoding. + batchsize : int, optional + The batch size for the generated grid (default is 1). + + Returns + ------- + torch.Tensor + A tensor of shape (batchsize, H*W, ref*ref) containing the positional encodings, + where H and W are the spatial dimensions from self.structured_shape. + """ + size_x, size_y = self.structured_shape gridx = torch.tensor(np.linspace(0, 1, size_x), dtype=torch.float) gridx = gridx.reshape(1, size_x, 1, 1).repeat([batchsize, 1, size_y, 1]) gridy = torch.tensor(np.linspace(0, 1, size_y), dtype=torch.float) gridy = gridy.reshape(1, 1, size_y, 1).repeat([batchsize, size_x, 1, 1]) grid = torch.cat((gridx, gridy), dim=-1) # B H W 2 - gridx = torch.tensor(np.linspace(0, 1, self.ref), dtype=torch.float) - gridx = gridx.reshape(1, self.ref, 1, 1).repeat([batchsize, 1, self.ref, 1]) - gridy = torch.tensor(np.linspace(0, 1, self.ref), dtype=torch.float) - gridy = gridy.reshape(1, 1, self.ref, 1).repeat([batchsize, self.ref, 1, 1]) + gridx = torch.tensor(np.linspace(0, 1, ref), dtype=torch.float) + gridx = gridx.reshape(1, ref, 1, 1).repeat([batchsize, 1, ref, 1]) + gridy = torch.tensor(np.linspace(0, 1, ref), dtype=torch.float) + gridy = gridy.reshape(1, 1, ref, 1).repeat([batchsize, ref, 1, 1]) grid_ref = torch.cat((gridx, gridy), dim=-1) # B H W 8 8 2 pos = ( @@ -287,33 +603,66 @@ def get_grid(self, batchsize=1): dim=-1, ) ) - .reshape(batchsize, size_x, size_y, self.ref * self.ref) + .reshape(batchsize, -1, ref * ref) # Flatten spatial dims .contiguous() ) return pos - def forward(self, x, fx, T=None): + def forward( + self, + fx: torch.Tensor, + embedding: torch.Tensor | None = None, + T: torch.Tensor = None, + ) -> torch.Tensor: + """ + Forward pass of the transolver model. + + For structured data, you may pass the functional input and embedding + as either [B, N, C] shape or [B, *structure, C]. Output will be returned + in the same shape as input. + + Unstructured data must be passed as [B, N, C] for both embeddings and tokens. + + Sublayers expect shape of [B, N, C] + + """ with torch.autograd.profiler.record_function("prepeocess"): + + # Reshape automatically, if necessary: + if self.structured_shape is not None: + unflatten_output = False + if len(fx.shape) != 3: + unflatten_output = True + fx = fx.reshape(fx.shape[0], -1, fx.shape[-1]) + if embedding is not None and len(embedding.shape) != 3: + embedding = embedding.reshape( + embedding.shape[0], *self.structured_shape, -1 + ) + if self.unified_pos: - if self.pos.device != x.device: - # move the position tensor: - self.pos = self.pos.to(x.device) - - x = ( - self.pos.repeat(x.shape[0], 1, 1, 1) - .reshape(x.shape[0], self.H * self.W, self.ref * self.ref) - .to(x.device) + # Extend the embedding to the batch size: + embedding = ( + self.embedding.repeat(embedding.shape[0], 1, 1) + # .reshape(x.shape[0], -1, self.embedding_dim) ) - if fx is not None: - fx = torch.cat((x, fx), -1) - fx = self.preprocess(fx) - else: - fx = self.preprocess(x) - fx = fx + self.placeholder[None, None, :] + + # Combine the embedding and functional input: + fx = torch.cat((embedding, fx), -1) + + # Dead code? Not sure when this is used. + # if fx is not None: + # else: + # fx = self.preprocess(x) + # fx = fx + self.placeholder[None, None, :] + + # Apply preprocessing + fx = self.preprocess(fx) if T is not None: with torch.autograd.profiler.record_function("Time_Embedding"): - Time_emb = timestep_embedding(T, self.n_hidden).repeat(1, x.shape[1], 1) + Time_emb = timestep_embedding(T, self.n_hidden).repeat( + 1, embedding.shape[1], 1 + ) Time_emb = self.time_fc(Time_emb) fx = fx + Time_emb @@ -321,133 +670,118 @@ def forward(self, x, fx, T=None): with torch.autograd.profiler.record_function(f"Block_{i}"): fx = block(fx) - return fx - + if self.structured_shape is not None: + if unflatten_output: + fx = fx.reshape(fx.shape[0], *self.structured_shape, -1) -@dataclass -class MetaData(ModelMetaData): - name: str = "Transolver" - # Optimization - jit: bool = False - cuda_graphs: bool = False - amp: bool = False - # Inference - onnx_cpu: bool = False # No FFT op on CPU - onnx_gpu: bool = True - onnx_runtime: bool = True - # Physics informed - var_dim: int = 1 - func_torch: bool = False - auto_grad: bool = False - - -class Transolver(Module): - """Transformer-based solver for PDEs. - - Note - ---- - Transolver is a model specifically designed for structured 2D mesh data. - - Parameters - ---------- - space_dim : int - The spatial dimension of the input data. - n_layers : int - The number of transformer layers. - n_hidden : int - The hidden dimension of the transformer. - dropout : float - The dropout rate. - n_head : int - The number of attention heads. - Time_Input : bool - Whether to include time embeddings. - act : str - The activation function. - mlp_ratio : int - The ratio of hidden dimension in the MLP. - fun_dim : int - The dimension of the function. - out_dim : int - The output dimension. - slice_num : int - The number of slices in the structured attention. - ref : int - The reference dimension. - unified_pos : bool - Whether to use unified positional embeddings. - H : int - The height of the mesh. - W : int - The width of the mesh. - use_te: bool - Whether to use transformer engine backend when possible. - """ - - def __init__( - self, - space_dim: int, - n_layers: int, - n_hidden: int, - dropout: float, - n_head: int, - Time_Input: bool, - act: str, - mlp_ratio: int, - fun_dim: int, - out_dim: int, - slice_num: int, - ref: int, - unified_pos: bool, - H: int, - W: int, - use_te: bool = True, - ) -> None: - super().__init__(meta=MetaData()) - self.H = H - self.W = W - self.model = Model( - space_dim=space_dim, - n_layers=n_layers, - n_hidden=n_hidden, - dropout=dropout, - n_head=n_head, - Time_Input=Time_Input, - act=act, - mlp_ratio=mlp_ratio, - fun_dim=fun_dim, - out_dim=out_dim, - slice_num=slice_num, - ref=ref, - unified_pos=unified_pos, - H=H, - W=W, - use_te=use_te, - ) - - def forward( - self, x: torch.Tensor, fx: torch.Tensor = None, T: torch.Tensor = None - ) -> torch.Tensor: - """Forward pass. - - Parameters - ---------- - x : torch.Tensor - The input tensor. - fx : torch.Tensor - The function tensor. - T : torch.Tensor - The time tensor. + return fx - Returns - ------- - torch.Tensor - The output tensor. - """ - # print(f"Input x shape: {x.shape}") - # print(f"Input fx shape: {fx.shape if fx is not None else None}") - # print(f"Input T shape: {T.shape if T is not None else None}") - y = self.model(x, fx, T) - y = y.reshape(x.shape[0], self.H, self.W, -1) - return y +# class Transolver(Module): +# """Transformer-based solver for PDEs. + +# Note +# ---- +# Transolver is a model specifically designed for structured 2D mesh data. + +# Parameters +# ---------- +# space_dim : int +# The spatial dimension of the input data. +# n_layers : int +# The number of transformer layers. +# n_hidden : int +# The hidden dimension of the transformer. +# dropout : float +# The dropout rate. +# n_head : int +# The number of attention heads. +# Time_Input : bool +# Whether to include time embeddings. +# act : str +# The activation function. +# mlp_ratio : int +# The ratio of hidden dimension in the MLP. +# fun_dim : int +# The dimension of the function. +# out_dim : int +# The output dimension. +# slice_num : int +# The number of slices in the structured attention. +# ref : int +# The reference dimension. +# unified_pos : bool +# Whether to use unified positional embeddings. +# structured_shape : None | tuple(int) +# The shape of the latent space. If None, assumes irregulare latent space. +# use_te: bool +# Whether to use transformer engine backend when possible. +# """ + +# def __init__( +# self, +# space_dim: int, +# n_layers: int, +# n_hidden: int, +# dropout: float, +# n_head: int, +# Time_Input: bool, +# act: str, +# mlp_ratio: int, +# fun_dim: int, +# out_dim: int, +# slice_num: int, +# ref: int, +# unified_pos: bool, +# H: int, +# W: int, +# use_te: bool = True, +# ) -> None: +# super().__init__(meta=MetaData()) +# self.H = H +# self.W = W +# self.model = Model( +# space_dim=space_dim, +# n_layers=n_layers, +# n_hidden=n_hidden, +# dropout=dropout, +# n_head=n_head, +# Time_Input=Time_Input, +# act=act, +# mlp_ratio=mlp_ratio, +# fun_dim=fun_dim, +# out_dim=out_dim, +# slice_num=slice_num, +# ref=ref, +# unified_pos=unified_pos, +# H=H, +# W=W, +# use_te=use_te, +# ) + +# def forward( +# self, x: torch.Tensor, fx: torch.Tensor = None, T: torch.Tensor = None +# ) -> torch.Tensor: +# """Forward pass. + +# Parameters +# ---------- +# x : torch.Tensor +# The input tensor. +# fx : torch.Tensor +# The function tensor. +# T : torch.Tensor +# The time tensor. + +# Returns +# ------- +# torch.Tensor +# The output tensor. + +# """ +# # print(f"Input x shape: {x.shape}") +# # print(f"Input fx shape: {fx.shape if fx is not None else None}") +# # print(f"Input T shape: {T.shape if T is not None else None}") +# y = self.model(x, fx, T) +# y = y.reshape(x.shape[0], self.H, self.W, -1) +# return y From c0517e567c5cae15097943443714ab86983cb34e Mon Sep 17 00:00:00 2001 From: Corey Adams Date: Wed, 2 Jul 2025 15:50:00 +0000 Subject: [PATCH 04/23] Beginning work on transolver example. Planning ahead for domain parallelism, this is a disk to gpu pipeline that natively optimizes bandwidth when domain parallel. --- .../transolver/README.md | 0 .../transolver/datapipe.py | 510 ++++++++++++++++++ 2 files changed, 510 insertions(+) create mode 100644 examples/cfd/external_aerodynamics/transolver/README.md create mode 100644 examples/cfd/external_aerodynamics/transolver/datapipe.py diff --git a/examples/cfd/external_aerodynamics/transolver/README.md b/examples/cfd/external_aerodynamics/transolver/README.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/cfd/external_aerodynamics/transolver/datapipe.py b/examples/cfd/external_aerodynamics/transolver/datapipe.py new file mode 100644 index 0000000000..c1de86b431 --- /dev/null +++ b/examples/cfd/external_aerodynamics/transolver/datapipe.py @@ -0,0 +1,510 @@ +# SPDX-FileCopyrightText: Copyright (c) 2023 - 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-FileCopyrightText: All rights reserved. +# SPDX-License-Identifier: MIT License +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" +Domain-parallel Zarr dataset for Transolver CFD data. + +This dataset implements domain parallelism where multiple ranks can read chunks +of data cooperatively from zarr files. It supports conversion to ShardTensor +objects for distributed training. +""" + +import os +import time +from concurrent.futures import ThreadPoolExecutor, Future +from pathlib import Path +from typing import Dict, List, Optional, Tuple, Union +from functools import lru_cache + +import numpy as np +import zarr + +import torch +import torch.distributed as dist +from torch.distributed.device_mesh import DeviceMesh +from torch.distributed.tensor.placement_types import Placement, Replicate, Shard +from torch.distributed.tensor._dtensor_spec import TensorMeta +from torch.utils.data import Dataset + +from physicsnemo.distributed import DistributedManager +from physicsnemo.distributed.shard_tensor import ShardTensor +from physicsnemo.distributed._shard_tensor_spec import ( + ShardTensorSpec, + _stride_from_contiguous_shape_C_style, +) +from physicsnemo.distributed.utils import compute_split_shapes +from physicsnemo.utils.profiling import profile + + +def get_filenames(data_path: Path, exclude_dirs: bool = True) -> List[str]: + """Get list of filenames from data directory.""" + + filenames = [] + for item in data_path.iterdir(): + # if exclude_dirs and item.is_dir(): + # continue + if item.suffix in [".zarr"]: + filenames.append(item.name) + return sorted(filenames) + + +def _read_chunk_into_array( + cpu_array: np.ndarray, + zarr_array: zarr.Array, + cpu_slice: slice, + zarr_slice: slice = None, +): + """Helper function to read a chunk from zarr into numpy array.""" + if zarr_slice is None: + zarr_slice = cpu_slice + cpu_array[cpu_slice] = zarr_array[zarr_slice] + + +@lru_cache +def to_torch_dtype(dtype: np.dtype) -> torch.dtype: + """Convert a numpy dtype to a torch dtype.""" + temp = torch.from_numpy(np.empty((), dtype=dtype)) + return temp.dtype + + +class DomainParallelZarrDataset(Dataset): + """ + PyTorch dataset for domain-parallel reading of Zarr files. + + This dataset supports: + - Efficient chunk-aligned reading for large data + - GPU memory streaming (disk -> numpy -> torch.Tensor -> GPU) + - Domain parallelism via DeviceMesh configuration + - Conversion to ShardTensor objects for domain-parallel usage + + This dataset supports multiple processes cooperating to read the data, and optimizations + for single process reading. + - The dataset will only read the keys from the `keys_to_read` parameter. + - For keys in `large_keys`, the dataset will spawn threads to read the data, + where each thread is aligned with a zarr chunk whenever possible. + - If domain parallelism is enabled, then the large and non-large keys may have alterate behavior: + - For large keys, each rank will participate in the read. Each rank will read a portion of the data, + designed to be approximately equal portions per process. Depending on the final output placement + of that key, it will be allgathered on the GPU (replicated) or converted inplace to ShardTensor. + - For small keys, each rank will read each key in its entirety. If the output placement is sharded, + the keys will be converted in place. A future update may enable round-robin reading, where each + process will read only a selection of keys and broadcast to partners. + + Technical details: + - CPU memory is allocated with torch, and if pin_memory is True, it's allocated directly into + pinned memory space. Numpy wraps the torch allocated memory, and Zarr streams the data into it. + - The threading is done on the zarr read only - typically the main bottleneck is the memory allocation + with pytorch. + - There might be a benefit to using threads on the torch alloc - but would require mutliple levels of threading. + + + Args: + data_path: Path to directory containing zarr files + device_mesh: Optional DeviceMesh for domain parallelism. If None, reads full data locally. + placements: Placement specifications for sharding. Must match mesh dimensions. + max_workers: Maximum number of threads for parallel I/O + pin_memory: Whether to pin the CPU memory of the allocated tensors. If True, the memory is allocated + in pinned memory space. If False, the memory is allocated in the default memory space. + Pinning is faster for GPU transfers, but the available pinned memory is smaller. + keys_to_read: Specific keys to read from zarr files. If None, reads all standard keys. + large_keys: Keys that should use chunk-aligned reading (example: high res volume data) + """ + + def __init__( + self, + data_path: Union[str, Path], + device_mesh: DeviceMesh | None = None, + placements: Placement | dict[str, Placement] | None = None, + max_workers: int = 4, + pin_memory: bool = True, + keys_to_read: list[str] | None = None, + large_keys: set[str] | None = None, + ): + super().__init__() + + # Initialize distributed manager if not already done + if not DistributedManager.is_initialized(): + DistributedManager.initialize() + + self.dm = DistributedManager() + self.data_path = Path(data_path) if isinstance(data_path, str) else data_path + self.device_mesh = device_mesh + self.placements = placements + self.max_workers = max_workers + self.pin_memory = pin_memory + self.keys_to_read = keys_to_read or set() + self.large_keys = large_keys or set() + + # Validate distributed configuration + if self.device_mesh is not None: + if self.device_mesh.ndim != 1: + raise ValueError( + "DomainParallelZarrDataset requires a single axis DeviceMesh if used" + ) + if self.placements is None: + raise ValueError( + "placements must be specified when device_mesh is provided" + ) + if isinstance(self.placements, dict): + for key, placement in self.placements.items(): + if len(placement) != 1: + raise ValueError( + f"placements must be a single placement for each key, got {placement} for key {key}" + ) + # Check that each key to read has a placement: + for key in self.keys_to_read: + if key not in self.placements: + raise ValueError( + f"placements must specify a placement for each key to read, missing placement for key {key}" + ) + elif isinstance(self.placements, tuple): + if len(self.placements) != 1: + raise ValueError( + f"placements must be a length-1 tuple of placement if not a dict, got {self.placements}" + ) + else: + raise ValueError( + f"placements must be a dict or tuple, got {type(self.placements)}" + ) + + # Get list of zarr files + self.filenames = get_filenames(self.data_path, exclude_dirs=True) + if not self.filenames: + raise ValueError(f"No zarr files found in {self.data_path}") + + # Create thread pool for parallel I/O + self.executor = ThreadPoolExecutor(max_workers=self.max_workers) + + # We cache ShardTensorSpecs for each tensor read, based on how they are + # read and not how they are meant to be. They get converted in the end. + self.tensor_specs = {} + + def __len__(self) -> int: + return len(self.filenames) + + def __del__(self): + """Clean up thread pool on destruction.""" + if hasattr(self, "executor"): + self.executor.shutdown(wait=True) + + @profile + def _read_key_chunk_aligned( + self, zarr_group: zarr.Group, key: str, futures: list[Future] + ) -> np.ndarray: + """ + Read a key using chunk-aligned I/O for efficiency. + Implements domain parallelism if device_mesh is configured. + """ + + zarr_array = zarr_group[key] + + # Determine what slice this rank should read + if self.device_mesh is not None: + # How many splits to make? + n_splits = dist.get_world_size(group=self.device_mesh.get_group()) + # What rank is this one? + this_rank = self.device_mesh.get_local_rank() + + sections = compute_split_shapes(zarr_array.shape[0], n_splits) + + global_chunk_start = sum(sections[:this_rank]) + global_chunk_stop = global_chunk_start + sections[this_rank] + + chunk_sizes = tuple( + (section,) + zarr_array.shape[1:] for section in sections + ) + stride = _stride_from_contiguous_shape_C_style(zarr_array.shape) + + meta = TensorMeta(zarr_array.shape, stride, zarr_array.dtype) + self.tensor_specs[key] = ShardTensorSpec( + mesh=self.device_mesh, + placements=(Shard(0),), + tensor_meta=meta, + _sharding_shapes={0: chunk_sizes}, + ) + + else: + global_chunk_start, global_chunk_stop = 0, zarr_array.shape[0] + + # Calculate the shape of data this rank will read + local_shape = [global_chunk_stop - global_chunk_start] + list( + zarr_array.shape[1:] + ) + + # Pre-allocate result array + torch_output = torch.empty( + local_shape, + dtype=to_torch_dtype(zarr_array.dtype), + pin_memory=self.pin_memory, + ) + # Share the memory buffer with numpy: + result = torch_output.numpy() + + # For chunk-aligned reading, align with zarr's chunk boundaries + # This is easiest when we precompute start/end slices + zarr_chunk_size = zarr_array.chunks[0] + + # Generate the global list of chunk boundaries first (then apply corrections) + slice_starts = list(range(0, zarr_array.shape[0], zarr_chunk_size)) + slice_stops = [start + zarr_chunk_size for start in slice_starts] + + # Correct the last stop: + slice_stops[-1] = zarr_array.shape[0] + + # Now, select all the slices that this rank is responsible for: + # These are all the boundary points exclusively within the global chunk slice: + local_boundaries = [ + s for s in slice_starts if s >= global_chunk_start and s < global_chunk_stop + ] + + # Fix the boundaries: + if global_chunk_start not in local_boundaries: + zarr_slice_starts = [global_chunk_start] + local_boundaries + else: + zarr_slice_starts = [s for s in local_boundaries] + + # The stops are the +1 locations + zarr_slice_stops = [s for s in zarr_slice_starts[1:]] + + # Handle the end situation + if global_chunk_stop not in zarr_slice_stops: + zarr_slice_stops.append(global_chunk_stop) + else: + # It's already in the list of boundaries + # We have to reduce the list of starts: + zarr_slice_starts = zarr_slice_starts[:-1] + + slice_sizes = [ + stop - start for stop, start in zip(zarr_slice_stops, zarr_slice_starts) + ] + + cpu_slice_starts = [0] + cpu_slice_stops = [slice_sizes[0]] + for slice_size in slice_sizes[1:]: + cpu_slice_starts.append(cpu_slice_stops[-1]) + cpu_slice_stops.append(cpu_slice_starts[-1] + slice_size) + + # Now, spawn threads to do each of those reads. + + for i in range(len(slice_sizes)): + + zarr_slice = np.s_[zarr_slice_starts[i] : zarr_slice_stops[i]] + cpu_slice = np.s_[cpu_slice_starts[i] : cpu_slice_stops[i]] + + future = self.executor.submit( + _read_chunk_into_array, + result, + zarr_array, + cpu_slice, + zarr_slice, + ) + futures.append(future) + + return torch_output + + @profile + def _read_key_standard( + self, zarr_group: zarr.Group, key: str, futures: list[Future] + ) -> torch.Tensor: + """Read a key with simple I/O (for small arrays).""" + zarr_array = zarr_group[key] + + # Handle scalar values + if zarr_array.shape == (): + + if self.device_mesh is not None: + self.tensor_specs[key] = ShardTensorSpec( + mesh=self.device_mesh, + placements=(Replicate(),), + tensor_meta=TensorMeta(zarr_array.shape, (), zarr_array.dtype), + _sharding_shapes={}, + ) + + output = torch.from_numpy(np.array(zarr_array)) + if self.pin_memory: + output = output.pin_memory() + return output + + # data = np.empty(zarr_array.shape, dtype=zarr_array.dtype) + output = torch.empty( + zarr_array.shape, + dtype=to_torch_dtype(zarr_array.dtype), + pin_memory=self.pin_memory, + ) + data = output.numpy() + slice = np.s_[:] + futures.append( + self.executor.submit( + _read_chunk_into_array, + data, + zarr_array, + slice, + ) + ) + + if self.device_mesh is not None: + # We need to track the tensor meta for this key + stride = _stride_from_contiguous_shape_C_style(zarr_array.shape) + + self.tensor_specs[key] = ShardTensorSpec( + mesh=self.device_mesh, + placements=(Replicate(),), + tensor_meta=TensorMeta(zarr_array.shape, stride, zarr_array.dtype), + _sharding_shapes={}, + ) + + return output + + @profile + def _read_zarr_file(self, filepath: Path) -> Dict[str, np.ndarray]: + """Read data from a zarr file.""" + with zarr.open_group(filepath, mode="r") as zarr_group: + data = {} + futures = [] + + # Process each key + for key in self.keys_to_read: + + if key not in zarr_group: + continue + + if key in self.large_keys: + # Use chunk-aligned reading for large data + data[key] = self._read_key_chunk_aligned(zarr_group, key, futures) + else: + # Use simple reading for other data + data[key] = self._read_key_standard(zarr_group, key, futures) + + # Wait for all futures to complete + for future in futures: + future.result() + + return data + + @profile + def _move_to_gpu(self, data: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]: + """Convert numpy arrays to torch tensors and move to GPU if available.""" + result = {} + + for key, array in data.items(): + # Move to GPU if available + if self.dm.cuda: + result[key] = data[key].to(self.dm.device, non_blocking=True) + + return result + + def _convert_to_shard_tensors( + self, tensors: Dict[str, torch.Tensor] + ) -> Dict[str, Union[torch.Tensor, ShardTensor]]: + """Convert tensors to ShardTensor objects for distributed training.""" + if self.device_mesh is None: + return tensors + + result = {} + + for key, tensor in tensors.items(): + + # Create a ShardTensor with whatever layout the data is actually in: + st = ShardTensor.__new__( + ShardTensor, + local_tensor=tensor, + spec=self.tensor_specs[key], + requires_grad=False, # By default, the data pipe output doesn't need a grad. + ) + + # Find out the desired placement: + if isinstance(self.placements, dict): + target_placement = self.placements[key] + else: + target_placement = self.placements + + # Redistribute if necessary: + # (Recall that this is one dimensional mesh only) + if st._spec.placements[0] != target_placement[0]: + st = st.redistribute(placements=target_placement) + + result[key] = st + + return result + + @profile + def __getitem__(self, idx: int) -> Dict[str, torch.Tensor | ShardTensor]: + """ + Get a data sample. + + Args: + idx: Index of the sample to retrieve + + Returns: + Dictionary containing tensors/ShardTensors for the requested data + """ + if idx >= len(self.filenames): + raise IndexError( + f"Index {idx} out of range for dataset of size {len(self.filenames)}" + ) + + filename = self.filenames[idx] + filepath = self.data_path / filename + + # Read data from zarr file + data = self._read_zarr_file(filepath) + + # Convert to torch tensors + tensors = self._move_to_gpu(data) + + # Convert to ShardTensors if using domain parallelism + if self.device_mesh is not None: + tensors = self._convert_to_shard_tensors(tensors) + + return tensors + + +# TODO: Additional features to consider implementing: +# 1. Caching of metadata to avoid repeated zarr.open_group calls +# 2. Asynchronous prefetching of next sample while processing current +# 3. More sophisticated sharding strategies (e.g., spatial partitioning) +# 4. Support for different placement strategies per key +# 5. Memory-mapped reading for very large datasets +# 6. Compression/decompression handling + + +def create_transolver_dataset( + data_path: Union[str, Path], + device_mesh: Optional[DeviceMesh] = None, + placements: Optional[Tuple[Placement, ...]] = None, + **kwargs, +) -> DomainParallelZarrDataset: + """ + Factory function to create a Transolver dataset with sensible defaults. + + Args: + data_path: Path to zarr data directory + device_mesh: Optional device mesh for domain parallelism + placements: Optional placement specifications + **kwargs: Additional arguments passed to DomainParallelZarrDataset + + Returns: + Configured dataset instance + """ + return DomainParallelZarrDataset( + data_path=data_path, device_mesh=device_mesh, placements=placements, **kwargs + ) From a07016572f288b9a844277cd2c03f86942b58d8d Mon Sep 17 00:00:00 2001 From: Corey Adams Date: Wed, 2 Jul 2025 16:01:05 +0000 Subject: [PATCH 05/23] Updates for the transolver example using the consolidated and updated model code. --- examples/cfd/darcy_transolver/config_fix.yaml | 22 +++++++++---------- .../train_transolver_darcy.py | 14 +++++++----- .../train_transolver_darcy_fix.py | 19 ++++++++-------- 3 files changed, 28 insertions(+), 27 deletions(-) diff --git a/examples/cfd/darcy_transolver/config_fix.yaml b/examples/cfd/darcy_transolver/config_fix.yaml index ce24362bfb..e90fb50567 100644 --- a/examples/cfd/darcy_transolver/config_fix.yaml +++ b/examples/cfd/darcy_transolver/config_fix.yaml @@ -21,29 +21,29 @@ # SOFTWARE. output_dir: ./output/darcy_transolver_fix -run_id: fp32_dev_fullres_b64_s64-te +run_id: fp16_dev_fullres_b8_s64-te data: train_path: /user_data/datasets/darcy_fix/example_data/piececonst_r421_N1024_smooth1.npz test_path: /user_data/datasets/darcy_fix/example_data/piececonst_r421_N1024_smooth2.npz resolution: 421 #421, 211, 141, 106, 85 all viable - batch_size: 32 # This is the GLOBAL batch size + batch_size: 8 # This is the GLOBAL batch size model: - space_dim: 2 + functional_dim: 1 + out_dim: 1 + embedding_dim: 2 n_layers: 4 - n_hidden: 128 + n_hidden: 64 dropout: 0.0 - n_head: 8 - Time_Input: False + n_head: 4 act: gelu - mlp_ratio: 1 - fun_dim: 1 - out_dim: 1 - ref: 8 + mlp_ratio: 2 unified_pos: False + ref: 8 slice_num: 64 - transformer_engine: True + use_te: True + Time_Input: False precision: fp32 diff --git a/examples/cfd/darcy_transolver/train_transolver_darcy.py b/examples/cfd/darcy_transolver/train_transolver_darcy.py index 325742abd0..734f5720b6 100644 --- a/examples/cfd/darcy_transolver/train_transolver_darcy.py +++ b/examples/cfd/darcy_transolver/train_transolver_darcy.py @@ -54,22 +54,24 @@ def darcy_trainer(cfg: DictConfig) -> None: # define model, loss, optimiser, scheduler, data loader model = Transolver( - space_dim=cfg.model.space_dim, + functional_dim=cfg.model.functional_dim, + out_dim=cfg.model.out_dim, + embedding_dim=cfg.model.embedding_dim, n_layers=cfg.model.n_layers, n_hidden=cfg.model.n_hidden, dropout=cfg.model.dropout, n_head=cfg.model.n_head, - Time_Input=cfg.model.Time_Input, act=cfg.model.act, mlp_ratio=cfg.model.mlp_ratio, fun_dim=cfg.model.fun_dim, - out_dim=cfg.model.out_dim, slice_num=cfg.model.slice_num, - ref=cfg.model.ref, unified_pos=cfg.model.unified_pos, - H=cfg.training.resolution, - W=cfg.training.resolution, + ref=cfg.model.ref, + structured_shape=[cfg.data.resolution, cfg.data.resolution], + use_te=cfg.model.use_te, + Time_Input=cfg.model.Time_Input, ).to(dist.device) + loss_fun = TestLoss(size_average=False) optimizer = Adam(model.parameters(), lr=cfg.scheduler.initial_lr) scheduler = lr_scheduler.LambdaLR( diff --git a/examples/cfd/darcy_transolver/train_transolver_darcy_fix.py b/examples/cfd/darcy_transolver/train_transolver_darcy_fix.py index 2eea776208..2b6461b470 100644 --- a/examples/cfd/darcy_transolver/train_transolver_darcy_fix.py +++ b/examples/cfd/darcy_transolver/train_transolver_darcy_fix.py @@ -72,7 +72,7 @@ def forward_train_full_loop( """ dm = DistributedManager() with context: - pred = model(pos, fx=x.unsqueeze(-1)).squeeze(-1) + pred = model(embedding=pos, fx=x.unsqueeze(-1)).squeeze(-1) pred = y_normalizer.decode(pred) loss = loss_fun(pred, y) if scaler is not None: @@ -125,7 +125,7 @@ def val_epoch(model, test_dataloader, loss_fun, y_normalizer): for i, batch in enumerate(test_dataloader): pos, x, y = batch with torch.no_grad(): - pred = model(pos, fx=x.unsqueeze(-1)).squeeze(-1) + pred = model(embedding=pos, fx=x.unsqueeze(-1)).squeeze(-1) pred = y_normalizer.decode(pred) loss = loss_fun(pred, y) @@ -188,22 +188,21 @@ def darcy_trainer(cfg: DictConfig) -> None: # define model ######################################################################## model = Transolver( - space_dim=cfg.model.space_dim, + functional_dim=cfg.model.functional_dim, + out_dim=cfg.model.out_dim, + embedding_dim=cfg.model.embedding_dim, n_layers=cfg.model.n_layers, n_hidden=cfg.model.n_hidden, dropout=cfg.model.dropout, n_head=cfg.model.n_head, - Time_Input=cfg.model.Time_Input, act=cfg.model.act, mlp_ratio=cfg.model.mlp_ratio, - fun_dim=cfg.model.fun_dim, - out_dim=cfg.model.out_dim, slice_num=cfg.model.slice_num, - ref=cfg.model.ref, unified_pos=cfg.model.unified_pos, - H=cfg.data.resolution, - W=cfg.data.resolution, - use_te=cfg.model.transformer_engine, + ref=cfg.model.ref, + structured_shape=[cfg.data.resolution, cfg.data.resolution], + use_te=cfg.model.use_te, + Time_Input=cfg.model.Time_Input, ).to(dm.device) if dm.world_size > 1: From b1aa22fb722bf0e0b5dd9cfb8f87283636c54f84 Mon Sep 17 00:00:00 2001 From: Corey Adams Date: Wed, 2 Jul 2025 16:05:44 +0000 Subject: [PATCH 06/23] Add small function to convert matlab matrices to npz --- .../darcy_transolver/convert_mat_to_npz.py | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 examples/cfd/darcy_transolver/convert_mat_to_npz.py diff --git a/examples/cfd/darcy_transolver/convert_mat_to_npz.py b/examples/cfd/darcy_transolver/convert_mat_to_npz.py new file mode 100644 index 0000000000..44125a71f8 --- /dev/null +++ b/examples/cfd/darcy_transolver/convert_mat_to_npz.py @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: Copyright (c) 2023 - 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-FileCopyrightText: All rights reserved. +# SPDX-License-Identifier: MIT License +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import sys +import numpy as np +from scipy.io import loadmat + + +def main(mat_file, npz_file): + # Load the .mat file + data = loadmat(mat_file) + + # Extract 'coeff' and 'sol' + coeff = data["coeff"] + sol = data["sol"] + + # Save to .npz file + np.savez(npz_file, coeff=coeff, sol=sol) + print(f"Saved 'coeff' and 'sol' to {npz_file}") + + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python convert_mat_to_npz.py input.mat") + sys.exit(1) + mat_file = sys.argv[1] + npz_file = mat_file.replace(".mat", ".npz") + main(mat_file, npz_file) From 25a37ab8be2da2bf41b4682ddbfd4589f9974d06 Mon Sep 17 00:00:00 2001 From: Corey Adams Date: Wed, 2 Jul 2025 16:06:30 +0000 Subject: [PATCH 07/23] Updates to the transolver model architecture --- physicsnemo/models/transolver/transolver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/physicsnemo/models/transolver/transolver.py b/physicsnemo/models/transolver/transolver.py index eaa2c1f49a..6a50f13695 100644 --- a/physicsnemo/models/transolver/transolver.py +++ b/physicsnemo/models/transolver/transolver.py @@ -498,6 +498,7 @@ def __init__( self.structured_shape = structured_shape # If we're using the unified position, create and save the position embeddings: + self.unified_pos = unified_pos if unified_pos: if structured_shape is None: From 4c383a3f6773c819d20ac92b3dd1df54e18f169f Mon Sep 17 00:00:00 2001 From: Corey Adams Date: Tue, 8 Jul 2025 19:56:18 +0000 Subject: [PATCH 08/23] Update transolver model. Add external aerodynamics example with transolver. --- .../transolver/conf/train.yaml | 82 ++++ .../external_aerodynamics/transolver/loss.py | 58 +++ .../transolver/metrics.py | 64 +++ .../external_aerodynamics/transolver/train.py | 439 ++++++++++++++++++ .../models/transolver/Physics_Attention.py | 278 +---------- physicsnemo/models/transolver/transolver.py | 268 +---------- 6 files changed, 653 insertions(+), 536 deletions(-) create mode 100644 examples/cfd/external_aerodynamics/transolver/conf/train.yaml create mode 100644 examples/cfd/external_aerodynamics/transolver/loss.py create mode 100644 examples/cfd/external_aerodynamics/transolver/metrics.py create mode 100644 examples/cfd/external_aerodynamics/transolver/train.py diff --git a/examples/cfd/external_aerodynamics/transolver/conf/train.yaml b/examples/cfd/external_aerodynamics/transolver/conf/train.yaml new file mode 100644 index 0000000000..37dc470d9e --- /dev/null +++ b/examples/cfd/external_aerodynamics/transolver/conf/train.yaml @@ -0,0 +1,82 @@ +# SPDX-FileCopyrightText: Copyright (c) 2023 - 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-FileCopyrightText: All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + + +output_dir: "outputs" +run_id: "transolver_te-1cycle" + +# Training configuration +training: + num_epochs: 400 + save_interval: 25 # Save checkpoint every N epochs + seed: 42 + output_dir: "outputs" + +# Model configuration +model: + _target_: physicsnemo.models.transolver.Transolver + functional_dim: 2 # Input feature dimension + out_dim: 4 # Output feature dimension + embedding_dim: 6 # Spatial embedding dimension + n_layers: 8 # Number of transformer layers + n_hidden: 256 # Hidden dimension + dropout: 0.0 # Dropout rate + n_head: 8 # Number of attention heads + act: "gelu" # Activation function + mlp_ratio: 4 # MLP ratio in attention blocks + slice_num: 128 # Number of slices in physics attention + unified_pos: false # Whether to use unified positional embeddings + ref: 8 # Reference dimension for unified pos + structured_shape: null + use_te: true # Use transformer engine + Time_Input: false # Whether to use time embeddings + + + +# Optimizer configuration +optimizer: + _target_: torch.optim.AdamW + lr: 1.0e-3 + weight_decay: 1.0e-4 + betas: [0.9, 0.999] + eps: 1.0e-8 + +# Data configuration +data: + train: + data_path: /group_data/datasets/drivaer_aws/domino/train/ + val: + data_path: /group_data/datasets/drivaer_aws/domino/val/ + max_workers: 32 + pin_memory: true + resolution: 262_144 + surface_keys: + - "surface_fields" + - "surface_mesh_centers" + - "surface_normals" + - "air_density" + - "stream_velocity" + +# Logging configuration +logging: + level: INFO + format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + +# Hardware configuration +hardware: + precision: float32 + compile_model: false # Whether to use torch.compile \ No newline at end of file diff --git a/examples/cfd/external_aerodynamics/transolver/loss.py b/examples/cfd/external_aerodynamics/transolver/loss.py new file mode 100644 index 0000000000..3420d7324c --- /dev/null +++ b/examples/cfd/external_aerodynamics/transolver/loss.py @@ -0,0 +1,58 @@ +# SPDX-FileCopyrightText: Copyright (c) 2023 - 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-FileCopyrightText: All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import torch +from typing import Literal + + +def loss_fn(pred, target): + return loss_fn_surface(pred, target, "mse") + + +def loss_fn_surface( + output: torch.Tensor, target: torch.Tensor, loss_type: Literal["mse", "rmse"] +) -> torch.Tensor: + """Calculate loss for surface data by handling scalar and vector components separately. + + Args: + output: Predicted surface values from the model + target: Ground truth surface values + loss_type: Type of loss to calculate ("mse" or "rmse") + + Returns: + Combined scalar and vector loss as a scalar tensor + """ + # Separate the scalar and vector components: + output_pressure, output_sheer = torch.split(output, [1, 3], dim=2) + target_pressure, target_sheer = torch.split(target, [1, 3], dim=2) + + numerator_pressure = torch.mean((output_pressure - target_pressure) ** 2.0) + numerator_sheer = torch.mean((target_sheer - output_sheer) ** 2.0, (0, 1)) + + if loss_type == "mse": + loss_pressure = numerator_pressure + loss_wall_sheer = torch.sum(numerator_sheer) + else: + denom = torch.mean((target_pressure) ** 2.0) + loss_pressure = numerator_pressure / denom + + # Compute the mean diff**2 of the vector component, leave the last dimension: + denom_sheer = torch.mean((target_sheer) ** 2.0, (0, 1)) + loss_wall_sheer = torch.sum(numerator_sheer / denom_sheer) + + loss = loss_pressure + loss_wall_sheer + + return loss / 4.0 diff --git a/examples/cfd/external_aerodynamics/transolver/metrics.py b/examples/cfd/external_aerodynamics/transolver/metrics.py new file mode 100644 index 0000000000..5bba878e2a --- /dev/null +++ b/examples/cfd/external_aerodynamics/transolver/metrics.py @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: Copyright (c) 2023 - 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-FileCopyrightText: All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import torch +import torch.distributed as dist + +from physicsnemo.distributed import DistributedManager + + +def all_reduce_dict(metrics, dm): + # TODO - update this to use domains and not the full world + + if dm.world_size == 1: + return metrics + + for key, value in metrics.items(): + dist.all_reduce(value) + value = value / dm.world_size + metrics[key] = value + + return metrics + + +def metrics_fn(pred, target, dm): + return metrics_fn_surface(pred, target, dm) + + +def metrics_fn_surface(pred, target, dm): + + l2_num = (pred - target) ** 2 + l2_num = torch.sum(l2_num, dim=1) + l2_num = torch.sqrt(l2_num) + + l2_denom = target**2 + l2_denom = torch.sum(l2_denom, dim=1) + l2_denom = torch.sqrt(l2_denom) + + l2 = l2_num / l2_denom + + metrics = { + "l2_pressure": torch.mean(l2[:, 0]), + "l2_sheer_x": torch.mean(l2[:, 1]), + "l2_sheer_y": torch.mean(l2[:, 2]), + "l2_sheer_z": torch.mean(l2[:, 3]), + } + + return metrics + + +def metrics_fn_surface_pressure(pred, target): + return torch.mean((pred - target) ** 2.0) diff --git a/examples/cfd/external_aerodynamics/transolver/train.py b/examples/cfd/external_aerodynamics/transolver/train.py new file mode 100644 index 0000000000..12b2e78708 --- /dev/null +++ b/examples/cfd/external_aerodynamics/transolver/train.py @@ -0,0 +1,439 @@ +# SPDX-FileCopyrightText: Copyright (c) 2023 - 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-FileCopyrightText: All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import time +import torch +import hydra +import omegaconf +from tabulate import tabulate +from omegaconf import DictConfig +from torch.utils.tensorboard import SummaryWriter + +from physicsnemo.launch.utils import load_checkpoint, save_checkpoint +from physicsnemo.launch.logging import PythonLogger, RankZeroLoggingWrapper +from physicsnemo.distributed import DistributedManager +from typing import Literal + +from physicsnemo.utils.profiling import profile, Profiler + +from datapipe import DomainParallelZarrDataset +from loss import loss_fn +from metrics import metrics_fn + + +@profile +def preprocess_data(batch): + + mesh_centers = batch["surface_mesh_centers"] + normals = batch["surface_normals"] + targets = batch["surface_fields"] + node_features = torch.stack( + [batch["air_density"], batch["stream_velocity"]], dim=-1 + ).to(torch.float32) + + # fourier_sin_features = [ + # torch.sin(mesh_centers * (2 ** i) * torch.pi) + # for i in range(4) + # ] + # fourier_cos_features = [ + # torch.cos(mesh_centers * (2 ** i) * torch.pi) + # for i in range(4) + # ] + + embeddings = torch.cat( + [ + mesh_centers, + normals, + # *fourier_sin_features, + # *fourier_cos_features + ], + dim=-1, + ) + + node_features = node_features.unsqueeze(0).broadcast_to(embeddings.shape[0], -1) + + return node_features, embeddings, targets + + +@profile +def downsample(features, embeddings, targets, num_keep=1024): + # Determine the number of samples to keep (e.g., 50% of original size) + num_samples = features.shape[0] + + # Generate random indices to keep + indices = torch.randperm(num_samples)[:num_keep] + + # Use the same indices to downsample all tensors + downsampled_features = features[indices] + downsampled_embeddings = embeddings[indices] + downsampled_targets = targets[indices] + + downsampled_features = downsampled_features.unsqueeze(0) + downsampled_embeddings = downsampled_embeddings.unsqueeze(0) + downsampled_targets = downsampled_targets.unsqueeze(0) + + return downsampled_features, downsampled_embeddings, downsampled_targets + + +@profile +def train_epoch( + dataloader, + sampler, + model, + optimizer, + scheduler, + logger, + writer, + epoch, + cfg, + dist_manager, +): + """Train for one epoch + + Args: + dataloader: Training data loader + model: The model to train + logger: Python logger instance + writer: Tensorboard writer + cfg: Configuration object + """ + model.train() + total_loss = 0 + total_metrics = {} + + epoch_indices = list(sampler) + epoch_len = len(epoch_indices) + + start_time = time.time() + for i, batch_idx in enumerate(epoch_indices): + batch = dataloader[batch_idx] + # Get data from batch + features, embeddings, targets = preprocess_data(batch) + + features, embeddings, targets = downsample( + features, embeddings, targets, cfg.data.resolution + ) + + # Forward pass + outputs = model(features, embeddings) + + metrics = metrics_fn(outputs, targets, dist_manager) + + if i == 0: + total_metrics = metrics + else: + total_metrics = { + k: total_metrics[k] + metrics[k].item() for k in metrics.keys() + } + + loss = loss_fn(outputs, targets) + + # Backward pass + optimizer.zero_grad() + loss.backward() + optimizer.step() + scheduler.step() + + end_time = time.time() + duration = end_time - start_time + start_time = end_time + + images_per_second = 1 / duration + + # Logging + total_loss += loss.item() + + logger.info( + f"Epoch {epoch} [{i}/{epoch_len}] Loss: {loss.item():.6f} Duration: {duration:.2f}s" + ) + if dist_manager.rank == 0: + writer.add_scalar( + "batch/learning_rate", + optimizer.param_groups[0]["lr"], + i + epoch_len * epoch, + ) + writer.add_scalar("batch/loss", loss.item(), i + epoch_len * epoch) + writer.add_scalar( + "batch/throughpu_per_gpu", images_per_second, i + epoch_len * epoch + ) + for metric_name, metric_value in metrics.items(): + writer.add_scalar( + f"batch/{metric_name}", metric_value, i + epoch_len * epoch + ) + + avg_loss = total_loss / epoch_len + avg_metrics = {k: v / epoch_len for k, v in total_metrics.items()} + if dist_manager.rank == 0: + writer.add_scalar("epoch/loss", avg_loss, epoch) + for metric_name, metric_value in avg_metrics.items(): + writer.add_scalar(f"epoch/{metric_name}", metric_value, epoch) + # Print average metrics using tabulate + metrics_table = tabulate( + [[k, v] for k, v in avg_metrics.items()], + headers=["Metric", "Average Value"], + tablefmt="pretty", + ) + print(f"\nEpoch {epoch} Average Metrics:\n{metrics_table}\n") + return avg_loss + + +@profile +def val_epoch(dataloader, sampler, model, logger, val_writer, epoch, cfg, dist_manager): + """Validation for one epoch + + Args: + dataloader: Validation data loader + sampler: Validation data sampler + model: The model to evaluate + logger: Python logger instance + writer: Tensorboard writer + epoch: Current epoch number + cfg: Configuration object + dist_manager: Distributed manager instance + """ + + model.eval() # Set model to evaluation mode + total_loss = 0 + total_metrics = {} + + epoch_indices = list(sampler) + epoch_len = len(epoch_indices) + + start_time = time.time() + with torch.no_grad(): # Disable gradient computation + for i, batch_idx in enumerate(epoch_indices): + batch = dataloader[batch_idx] + # Get data from batch + features, embeddings, targets = preprocess_data(batch) + + features, embeddings, targets = downsample( + features, embeddings, targets, cfg.data.resolution + ) + + # Forward pass + outputs = model(features, embeddings) + + metrics = metrics_fn(outputs, targets, dist_manager) + + if i == 0: + total_metrics = metrics + else: + total_metrics = { + k: total_metrics[k] + metrics[k].item() for k in metrics.keys() + } + + loss = loss_fn(outputs, targets) + + end_time = time.time() + duration = end_time - start_time + start_time = end_time + + # Logging + total_loss += loss.item() + + logger.info( + f"Val [{i}/{epoch_len}] Loss: {loss.item():.6f} Duration: {duration:.2f}s" + ) + # We don't add individual loss measurements in the validation loop. + + avg_loss = total_loss / epoch_len + avg_metrics = {k: v / epoch_len for k, v in total_metrics.items()} + if dist_manager.rank == 0: + val_writer.add_scalar("epoch/loss", avg_loss, epoch) + for metric_name, metric_value in avg_metrics.items(): + val_writer.add_scalar(f"epoch/{metric_name}", metric_value, epoch) + # Print average metrics using tabulate + metrics_table = tabulate( + [[k, v] for k, v in avg_metrics.items()], + headers=["Metric", "Average Value"], + tablefmt="pretty", + ) + print(f"\nEpoch {epoch} Validation Average Metrics:\n{metrics_table}\n") + return avg_loss + + +@profile +def main(cfg: DictConfig): + """Main training function + + Args: + cfg: Hydra configuration object + """ + + DistributedManager.initialize() + + # Set up distributed training + dist_manager = DistributedManager() + + # Set up logging + logger = RankZeroLoggingWrapper(PythonLogger(name="training"), dist_manager) + if dist_manager.rank == 0: + os.makedirs(cfg.output_dir, exist_ok=True) + writer = SummaryWriter( + log_dir=os.path.join( + cfg.output_dir + "/" + cfg.run_id + "/train", + ) + ) + val_writer = SummaryWriter( + log_dir=os.path.join( + cfg.output_dir + "/" + cfg.run_id + "/val", + ) + ) + else: + writer = None + val_writer = None + + logger.info(f"Config:\n{omegaconf.OmegaConf.to_yaml(cfg, resolve=True)}") + + # Set up model + model = hydra.utils.instantiate(cfg.model) + model.to(dist_manager.device) + + if dist_manager.world_size > 1: + model = torch.nn.parallel.DistributedDataParallel( + model, + device_ids=[dist_manager.local_rank], + output_device=dist_manager.device, + ) + + num_params = sum(p.numel() for p in model.parameters()) + logger.info(f"Number of parameters: {num_params}") + + # Set up data + device_mesh = None + placements = None + + # Training dataset + train_dataset = DomainParallelZarrDataset( + data_path=cfg.data.train.data_path, + device_mesh=device_mesh, + placements=placements, + max_workers=cfg.data.max_workers, + pin_memory=cfg.data.pin_memory, + keys_to_read=cfg.data.surface_keys, + large_keys=None, + ) + + # Validation dataset + val_dataset = DomainParallelZarrDataset( + data_path=cfg.data.val.data_path, # Assuming validation data path is configured + device_mesh=device_mesh, + placements=placements, + max_workers=cfg.data.max_workers, + pin_memory=cfg.data.pin_memory, + keys_to_read=cfg.data.surface_keys, + large_keys=None, + ) + + # Set up distributed samplers + train_sampler = torch.utils.data.distributed.DistributedSampler( + train_dataset, + num_replicas=dist_manager.world_size, + rank=dist_manager.rank, + shuffle=True, + drop_last=True, + ) + + val_sampler = torch.utils.data.distributed.DistributedSampler( + val_dataset, + num_replicas=dist_manager.world_size, + rank=dist_manager.rank, + shuffle=False, # No shuffling for validation + drop_last=True, + ) + + # Set up optimizer and scheduler + optimizer = hydra.utils.instantiate(cfg.optimizer, params=model.parameters()) + # Set up OneCycleLR learning rate scheduler + scheduler = torch.optim.lr_scheduler.OneCycleLR( + optimizer, + max_lr=cfg.optimizer.lr, + total_steps=len(list(train_sampler)) * cfg.training.num_epochs, + pct_start=0.3, + div_factor=25.0, + final_div_factor=1e4, + ) + + ckpt_args = { + "path": f"{cfg.output_dir}/runs/{cfg.run_id}/checkpoints", + "optimizer": optimizer, + "scheduler": scheduler, + "models": model, + } + loaded_epoch = load_checkpoint(device=dist_manager.device, **ckpt_args) + + # Training loop + logger.info("Starting training...") + for epoch in range(loaded_epoch, cfg.training.num_epochs): + # Set the epoch in the samplers + train_sampler.set_epoch(epoch) + val_sampler.set_epoch(epoch) + + # Training phase + train_loss = train_epoch( + train_dataset, + train_sampler, + model, + optimizer, + scheduler, + logger, + writer, + epoch, + cfg, + dist_manager, + ) + + # Validation phase + val_loss = val_epoch( + val_dataset, + val_sampler, + model, + logger, + val_writer, + epoch, + cfg, + dist_manager, + ) + + # Log epoch results + logger.info( + f"Epoch [{epoch}/{cfg.training.num_epochs}] Train Loss: {train_loss:.6f} Val Loss: {val_loss:.6f}" + ) + + # save checkpoint + if epoch % cfg.training.save_interval == 0 and dist_manager.rank == 0: + save_checkpoint(**ckpt_args, epoch=epoch) + + logger.info("Training completed!") + + +@hydra.main(version_base=None, config_path="conf", config_name="train") +def launch(cfg: DictConfig): + """Launch training with hydra configuration + + Args: + cfg: Hydra configuration object + """ + # profiler = Profiler() + # profiler.enable("line_profiler") + # profiler.initialize() + main(cfg) + # profiler.finalize() + + +if __name__ == "__main__": + launch() diff --git a/physicsnemo/models/transolver/Physics_Attention.py b/physicsnemo/models/transolver/Physics_Attention.py index c303634391..ab1409eb21 100644 --- a/physicsnemo/models/transolver/Physics_Attention.py +++ b/physicsnemo/models/transolver/Physics_Attention.py @@ -67,7 +67,6 @@ def __init__(self, dim, heads, dim_head, dropout, slice_num, use_te): self.in_project_slice = nn.Linear(dim_head, slice_num) for l_i in [self.in_project_slice]: torch.nn.init.orthogonal_(l_i.weight) # use a principled initialization - if not use_te: self.to_q = nn.Linear(dim_head, dim_head, bias=False) self.to_k = nn.Linear(dim_head, dim_head, bias=False) @@ -228,61 +227,31 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: # Project the inputs onto learned spaces: x_mid, fx_mid = self.project_input_onto_slices(x) - # if (x_mid.isnan().any()): - # print(f"Rank {dm.rank} x_mid is nan") - # exit() - # if (fx_mid.isnan().any()): - # print(f"Rank {dm.rank} fx_mid is nan") - # exit() - # x_mid and fx_mid should have shapes of [B, N_head, N_tokens, Head_dim] # Perform the linear projection of learned latent space onto slices: slice_projections = self.in_project_slice(x_mid) - # if (slice_projections.isnan().any()): - # print(f"Rank {dm.rank} slice_projections is nan") - # exit() + # Slice projections has shape [B, N_head, N_tokens, Head_dim], but head_dim may have changed! # Use the slice projections and learned spaces to compute the slices, and their weights: slice_weights, slice_tokens = self.compute_slices_from_projections( slice_projections, fx_mid ) - # if (slice_weights.isnan().any()): - # print(f"Rank {dm.rank} slice_weights is nan") - # exit() - # if (slice_tokens.isnan().any()) or (slice_tokens.isinf().any()): - # print(f"Rank {dm.rank} slice_tokens is nan or inf") - # torch.save(slice_tokens, f"slice_tokens_{dm.rank}.pt") - # torch.save(slice_weights, f"slice_weights_{dm.rank}.pt") - # torch.save(slice_projections, f"slice_projections_{dm.rank}.pt") - # torch.save(fx_mid, f"fx_mid_{dm.rank}.pt") - # exit() # slice_weights has shape [Batch, N_heads, N_tokens, Slice_num] # slice_tokens has shape [Batch, N_heads, N_tokens, head_dim] - # print(f"Rank {dm.rank} Min/max/std of slice tokens: {slice_tokens.min()}, {slice_tokens.max()}, {slice_tokens.std()}") - # Apply attention to the slice tokens if self.use_te: out_slice_token = self.compute_slice_attention_te(slice_tokens) - # out_slice_token = self.compute_slice_attention(slice_tokens) else: + # out_slice_token = self.compute_slice_attention(slice_tokens) out_slice_token = self.compute_slice_attention_sdpa(slice_tokens) - # if (out_slice_token.isnan().any()): - # print(f"Rank {dm.rank} out_slice_token is nan") - # torch.save(slice_tokens, f"slice_tokens_{dm.rank}.pt") - # torch.save(slice_weights, f"slice_weights_{dm.rank}.pt") - # torch.save(slice_projections, f"slice_projections_{dm.rank}.pt") - # torch.save(fx_mid, f"fx_mid_{dm.rank}.pt") - # exit() # Shape unchanged # Deslice: outputs = self.project_attention_outputs(out_slice_token, slice_weights) - # if (outputs.isnan().any()): - # print(f"Rank {dm.rank} outputs is nan") - # exit() + # Outputs now has the same shape as the original input x return outputs @@ -298,7 +267,6 @@ def __init__( ): super().__init__(dim, heads, dim_head, dropout, slice_num, use_te) inner_dim = dim_head * heads - self.in_project_x = nn.Linear(dim, inner_dim) self.in_project_fx = nn.Linear(dim, inner_dim) @@ -306,7 +274,6 @@ def project_input_onto_slices(self, x) -> tuple[torch.Tensor, torch.Tensor]: """ Project the input onto the slice space. """ - fx_mid = rearrange( self.in_project_fx(x), "B N (h d) -> B h N d", h=self.heads, d=self.dim_head ) @@ -421,242 +388,3 @@ def project_input_onto_slices(self, x) -> tuple[torch.Tensor, torch.Tensor]: ) return input_projected_x, input_projected_fx - - -class Physics_Attention_Irregular_Mesh(nn.Module): - "for irregular meshes in 1D, 2D or 3D space" - - def __init__(self, dim, heads=8, dim_head=64, dropout=0.0, slice_num=64): - super().__init__() - inner_dim = dim_head * heads - self.dim_head = dim_head - self.heads = heads - self.scale = dim_head**-0.5 - self.softmax = nn.Softmax(dim=-1) - self.dropout = nn.Dropout(dropout) - self.temperature = nn.Parameter(torch.ones([1, heads, 1, 1]) * 0.5) - - self.in_project_x = nn.Linear(dim, inner_dim) - self.in_project_fx = nn.Linear(dim, inner_dim) - self.in_project_slice = nn.Linear(dim_head, slice_num) - for l_i in [self.in_project_slice]: - torch.nn.init.orthogonal_(l_i.weight) # use a principled initialization - self.to_q = nn.Linear(dim_head, dim_head, bias=False) - self.to_k = nn.Linear(dim_head, dim_head, bias=False) - self.to_v = nn.Linear(dim_head, dim_head, bias=False) - self.to_out = nn.Sequential(nn.Linear(inner_dim, dim), nn.Dropout(dropout)) - - def forward(self, x): - # B N C - B, N, C = x.shape - - ### (1) Slice - fx_mid = ( - self.in_project_fx(x) - .reshape(B, N, self.heads, self.dim_head) - .permute(0, 2, 1, 3) - .contiguous() - ) # B H N C - x_mid = ( - self.in_project_x(x) - .reshape(B, N, self.heads, self.dim_head) - .permute(0, 2, 1, 3) - .contiguous() - ) # B H N C - slice_weights = self.softmax( - self.in_project_slice(x_mid) / self.temperature - ) # B H N G - slice_norm = slice_weights.sum(2) # B H G - slice_token = torch.einsum("bhnc,bhng->bhgc", fx_mid, slice_weights) - slice_token = slice_token / ( - (slice_norm + 1e-5)[:, :, :, None].repeat(1, 1, 1, self.dim_head) - ) - - ### (2) Attention among slice tokens - q_slice_token = self.to_q(slice_token) - k_slice_token = self.to_k(slice_token) - v_slice_token = self.to_v(slice_token) - dots = torch.matmul(q_slice_token, k_slice_token.transpose(-1, -2)) * self.scale - attn = self.softmax(dots) - attn = self.dropout(attn) - out_slice_token = torch.matmul(attn, v_slice_token) # B H G D - - ### (3) Deslice - out_x = torch.einsum("bhgc,bhng->bhnc", out_slice_token, slice_weights) - out_x = rearrange(out_x, "b h n d -> b n (h d)") - return self.to_out(out_x) - - -class Physics_Attention_Structured_Mesh_2D(nn.Module): - "for structured mesh in 2D space" - - def __init__( - self, - dim, - heads=8, - dim_head=64, - dropout=0.0, - slice_num=64, - H=101, - W=31, - kernel=3, - ): # kernel=3): - super().__init__() - inner_dim = dim_head * heads - self.dim_head = dim_head - self.heads = heads - self.scale = dim_head**-0.5 - self.softmax = nn.Softmax(dim=-1) - self.dropout = nn.Dropout(dropout) - self.temperature = nn.Parameter(torch.ones([1, heads, 1, 1]) * 0.5) - self.H = H - self.W = W - - self.in_project_x = nn.Conv2d(dim, inner_dim, kernel, 1, kernel // 2) - self.in_project_fx = nn.Conv2d(dim, inner_dim, kernel, 1, kernel // 2) - self.in_project_slice = nn.Linear(dim_head, slice_num) - for l_i in [self.in_project_slice]: - torch.nn.init.orthogonal_(l_i.weight) # use a principled initialization - self.to_q = nn.Linear(dim_head, dim_head, bias=False) - self.to_k = nn.Linear(dim_head, dim_head, bias=False) - self.to_v = nn.Linear(dim_head, dim_head, bias=False) - - self.to_out = nn.Sequential(nn.Linear(inner_dim, dim), nn.Dropout(dropout)) - - def forward(self, x): - # B N C - B, N, C = x.shape - x = ( - x.reshape(B, self.H, self.W, C) - .contiguous() - .permute(0, 3, 1, 2) - .contiguous() - ) # B C H W - - ### (1) Slice - fx_mid = ( - self.in_project_fx(x) - .permute(0, 2, 3, 1) - .contiguous() - .reshape(B, N, self.heads, self.dim_head) - .permute(0, 2, 1, 3) - .contiguous() - ) # B H N C - x_mid = ( - self.in_project_x(x) - .permute(0, 2, 3, 1) - .contiguous() - .reshape(B, N, self.heads, self.dim_head) - .permute(0, 2, 1, 3) - .contiguous() - ) # B H N G - slice_weights = self.softmax( - self.in_project_slice(x_mid) / torch.clamp(self.temperature, min=0.1, max=5) - ) # B H N G - slice_norm = slice_weights.sum(2) # B H G - slice_token = torch.einsum("bhnc,bhng->bhgc", fx_mid, slice_weights) - slice_token = slice_token / ( - (slice_norm + 1e-5)[:, :, :, None].repeat(1, 1, 1, self.dim_head) - ) - - ### (2) Attention among slice tokens - q_slice_token = self.to_q(slice_token) - k_slice_token = self.to_k(slice_token) - v_slice_token = self.to_v(slice_token) - dots = torch.matmul(q_slice_token, k_slice_token.transpose(-1, -2)) * self.scale - attn = self.softmax(dots) - attn = self.dropout(attn) - out_slice_token = torch.matmul(attn, v_slice_token) # B H G D - - ### (3) Deslice - out_x = torch.einsum("bhgc,bhng->bhnc", out_slice_token, slice_weights) - out_x = rearrange(out_x, "b h n d -> b n (h d)") - return self.to_out(out_x) - - -class Physics_Attention_Structured_Mesh_3D(nn.Module): - "for structured mesh in 3D space" - - def __init__( - self, - dim, - heads=8, - dim_head=64, - dropout=0.0, - slice_num=32, - H=32, - W=32, - D=32, - kernel=3, - ): - super().__init__() - inner_dim = dim_head * heads - self.dim_head = dim_head - self.heads = heads - self.scale = dim_head**-0.5 - self.softmax = nn.Softmax(dim=-1) - self.dropout = nn.Dropout(dropout) - self.temperature = nn.Parameter(torch.ones([1, heads, 1, 1]) * 0.5) - self.H = H - self.W = W - self.D = D - - self.in_project_x = nn.Conv3d(dim, inner_dim, kernel, 1, kernel // 2) - self.in_project_fx = nn.Conv3d(dim, inner_dim, kernel, 1, kernel // 2) - self.in_project_slice = nn.Linear(dim_head, slice_num) - for l_i in [self.in_project_slice]: - torch.nn.init.orthogonal_(l_i.weight) # use a principled initialization - self.to_q = nn.Linear(dim_head, dim_head, bias=False) - self.to_k = nn.Linear(dim_head, dim_head, bias=False) - self.to_v = nn.Linear(dim_head, dim_head, bias=False) - self.to_out = nn.Sequential(nn.Linear(inner_dim, dim), nn.Dropout(dropout)) - - def forward(self, x): - # B N C - B, N, C = x.shape - x = ( - x.reshape(B, self.H, self.W, self.D, C) - .contiguous() - .permute(0, 4, 1, 2, 3) - .contiguous() - ) # B C H W - - ### (1) Slice - fx_mid = ( - self.in_project_fx(x) - .permute(0, 2, 3, 4, 1) - .contiguous() - .reshape(B, N, self.heads, self.dim_head) - .permute(0, 2, 1, 3) - .contiguous() - ) # B H N C - x_mid = ( - self.in_project_x(x) - .permute(0, 2, 3, 4, 1) - .contiguous() - .reshape(B, N, self.heads, self.dim_head) - .permute(0, 2, 1, 3) - .contiguous() - ) # B H N G - slice_weights = self.softmax( - self.in_project_slice(x_mid) / torch.clamp(self.temperature, min=0.1, max=5) - ) # B H N G - slice_norm = slice_weights.sum(2) # B H G - slice_token = torch.einsum("bhnc,bhng->bhgc", fx_mid, slice_weights) - slice_token = slice_token / ( - (slice_norm + 1e-5)[:, :, :, None].repeat(1, 1, 1, self.dim_head) - ) - - ### (2) Attention among slice tokens - q_slice_token = self.to_q(slice_token) - k_slice_token = self.to_k(slice_token) - v_slice_token = self.to_v(slice_token) - dots = torch.matmul(q_slice_token, k_slice_token.transpose(-1, -2)) * self.scale - attn = self.softmax(dots) - attn = self.dropout(attn) - out_slice_token = torch.matmul(attn, v_slice_token) # B H G D - - ### (3) Deslice - out_x = torch.einsum("bhgc,bhng->bhnc", out_slice_token, slice_weights) - out_x = rearrange(out_x, "b h n d -> b n (h d)") - return self.to_out(out_x) diff --git a/physicsnemo/models/transolver/transolver.py b/physicsnemo/models/transolver/transolver.py index 6a50f13695..6dbf16d3f7 100644 --- a/physicsnemo/models/transolver/transolver.py +++ b/physicsnemo/models/transolver/transolver.py @@ -205,153 +205,6 @@ def forward(self, fx): return fx -# class Model(nn.Module): -# def __init__( -# self, -# space_dim=1, -# n_layers=5, -# n_hidden=256, -# dropout=0.0, -# n_head=8, -# Time_Input=False, -# act="gelu", -# mlp_ratio=1, -# fun_dim=1, -# out_dim=1, -# slice_num=32, -# ref=8, -# unified_pos=False, -# H=85, -# W=85, -# use_te=True, -# ): -# super().__init__() -# self.__name__ = "Transolver_2D" -# self.H = H -# self.W = W -# self.ref = ref -# self.unified_pos = unified_pos -# if self.unified_pos: -# self.pos = self.get_grid() -# self.preprocess = MLP( -# fun_dim + self.ref * self.ref, -# n_hidden * 2, -# n_hidden, -# n_layers=0, -# res=False, -# act=act, -# ) -# else: -# self.preprocess = MLP( -# fun_dim + space_dim, -# n_hidden * 2, -# n_hidden, -# n_layers=0, -# res=False, -# act=act, -# ) - -# self.Time_Input = Time_Input -# self.n_hidden = n_hidden -# self.space_dim = space_dim -# if Time_Input: -# self.time_fc = nn.Sequential( -# nn.Linear(n_hidden, n_hidden), nn.SiLU(), nn.Linear(n_hidden, n_hidden) -# ) -# self.blocks = nn.ModuleList( -# [ -# Transolver_block( -# num_heads=n_head, -# hidden_dim=n_hidden, -# dropout=dropout, -# act=act, -# mlp_ratio=mlp_ratio, -# out_dim=out_dim, -# slice_num=slice_num, -# H=H, -# W=W, -# last_layer=(_ == n_layers - 1), -# use_te=use_te, -# ) -# for _ in range(n_layers) -# ] -# ) -# self.initialize_weights() -# # self.placeholder = nn.Parameter( -# # (1 / (n_hidden)) * torch.rand(n_hidden, dtype=torch.float) -# # ) - -# def initialize_weights(self): -# self.apply(self._init_weights) - -# def _init_weights(self, m): -# if isinstance(m, nn.Linear): -# nn.init.trunc_normal_(m.weight, std=0.02) -# if isinstance(m, nn.Linear) and m.bias is not None: -# nn.init.constant_(m.bias, 0) -# elif isinstance(m, (nn.LayerNorm, te.LayerNorm, nn.BatchNorm1d)): -# nn.init.constant_(m.bias, 0) -# nn.init.constant_(m.weight, 1.0) - -# def get_grid(self, batchsize=1): -# size_x, size_y = self.H, self.W -# gridx = torch.tensor(np.linspace(0, 1, size_x), dtype=torch.float) -# gridx = gridx.reshape(1, size_x, 1, 1).repeat([batchsize, 1, size_y, 1]) -# gridy = torch.tensor(np.linspace(0, 1, size_y), dtype=torch.float) -# gridy = gridy.reshape(1, 1, size_y, 1).repeat([batchsize, size_x, 1, 1]) -# grid = torch.cat((gridx, gridy), dim=-1) # B H W 2 - -# gridx = torch.tensor(np.linspace(0, 1, self.ref), dtype=torch.float) -# gridx = gridx.reshape(1, self.ref, 1, 1).repeat([batchsize, 1, self.ref, 1]) -# gridy = torch.tensor(np.linspace(0, 1, self.ref), dtype=torch.float) -# gridy = gridy.reshape(1, 1, self.ref, 1).repeat([batchsize, self.ref, 1, 1]) -# grid_ref = torch.cat((gridx, gridy), dim=-1) # B H W 8 8 2 - -# pos = ( -# torch.sqrt( -# torch.sum( -# (grid[:, :, :, None, None, :] - grid_ref[:, None, None, :, :, :]) -# ** 2, -# dim=-1, -# ) -# ) -# .reshape(batchsize, size_x, size_y, self.ref * self.ref) -# .contiguous() -# ) -# return pos - -# def forward(self, x, fx, T=None): -# with torch.autograd.profiler.record_function("prepeocess"): -# if self.unified_pos: -# if self.pos.device != x.device: -# # move the position tensor: -# self.pos = self.pos.to(x.device) - -# x = ( -# self.pos.repeat(x.shape[0], 1, 1, 1) -# .reshape(x.shape[0], self.H * self.W, self.ref * self.ref) -# .to(x.device) -# ) -# if fx is not None: -# fx = torch.cat((x, fx), -1) -# fx = self.preprocess(fx) -# else: -# fx = self.preprocess(x) -# fx = fx + self.placeholder[None, None, :] - -# if T is not None: -# with torch.autograd.profiler.record_function("Time_Embedding"): -# Time_emb = timestep_embedding(T, self.n_hidden).repeat(1, x.shape[1], 1) -# Time_emb = self.time_fc(Time_emb) -# fx = fx + Time_emb - -# for i, block in enumerate(self.blocks): -# with torch.autograd.profiler.record_function(f"Block_{i}"): -# fx = block(fx) - -# return fx - - @dataclass class MetaData(ModelMetaData): name: str = "Transolver" @@ -469,6 +322,7 @@ def __init__( super().__init__(meta=MetaData()) self.__name__ = "Transolver" + self.use_te = use_te # Check that the hidden dimension and head dimensions are compatible: if not n_hidden % n_head == 0: raise ValueError( @@ -550,6 +404,7 @@ def __init__( ] ) self.initialize_weights() + # self.placeholder = nn.Parameter( # (1 / (n_hidden)) * torch.rand(n_hidden, dtype=torch.float) # ) @@ -562,7 +417,10 @@ def _init_weights(self, m): nn.init.trunc_normal_(m.weight, std=0.02) if isinstance(m, nn.Linear) and m.bias is not None: nn.init.constant_(m.bias, 0) - elif isinstance(m, (nn.LayerNorm, te.LayerNorm, nn.BatchNorm1d)): + norm_layers = (nn.LayerNorm, nn.BatchNorm1d) + if self.use_te: + norm_layers = norm_layers + (te.LayerNorm,) + if isinstance(m, norm_layers): nn.init.constant_(m.bias, 0) nn.init.constant_(m.weight, 1.0) @@ -627,7 +485,7 @@ def forward( Sublayers expect shape of [B, N, C] """ - with torch.autograd.profiler.record_function("prepeocess"): + with torch.autograd.profiler.record_function("preprocess"): # Reshape automatically, if necessary: if self.structured_shape is not None: @@ -646,10 +504,8 @@ def forward( self.embedding.repeat(embedding.shape[0], 1, 1) # .reshape(x.shape[0], -1, self.embedding_dim) ) - # Combine the embedding and functional input: fx = torch.cat((embedding, fx), -1) - # Dead code? Not sure when this is used. # if fx is not None: # else: @@ -676,113 +532,3 @@ def forward( fx = fx.reshape(fx.shape[0], *self.structured_shape, -1) return fx - - -# class Transolver(Module): -# """Transformer-based solver for PDEs. - -# Note -# ---- -# Transolver is a model specifically designed for structured 2D mesh data. - -# Parameters -# ---------- -# space_dim : int -# The spatial dimension of the input data. -# n_layers : int -# The number of transformer layers. -# n_hidden : int -# The hidden dimension of the transformer. -# dropout : float -# The dropout rate. -# n_head : int -# The number of attention heads. -# Time_Input : bool -# Whether to include time embeddings. -# act : str -# The activation function. -# mlp_ratio : int -# The ratio of hidden dimension in the MLP. -# fun_dim : int -# The dimension of the function. -# out_dim : int -# The output dimension. -# slice_num : int -# The number of slices in the structured attention. -# ref : int -# The reference dimension. -# unified_pos : bool -# Whether to use unified positional embeddings. -# structured_shape : None | tuple(int) -# The shape of the latent space. If None, assumes irregulare latent space. -# use_te: bool -# Whether to use transformer engine backend when possible. -# """ - -# def __init__( -# self, -# space_dim: int, -# n_layers: int, -# n_hidden: int, -# dropout: float, -# n_head: int, -# Time_Input: bool, -# act: str, -# mlp_ratio: int, -# fun_dim: int, -# out_dim: int, -# slice_num: int, -# ref: int, -# unified_pos: bool, -# H: int, -# W: int, -# use_te: bool = True, -# ) -> None: -# super().__init__(meta=MetaData()) -# self.H = H -# self.W = W -# self.model = Model( -# space_dim=space_dim, -# n_layers=n_layers, -# n_hidden=n_hidden, -# dropout=dropout, -# n_head=n_head, -# Time_Input=Time_Input, -# act=act, -# mlp_ratio=mlp_ratio, -# fun_dim=fun_dim, -# out_dim=out_dim, -# slice_num=slice_num, -# ref=ref, -# unified_pos=unified_pos, -# H=H, -# W=W, -# use_te=use_te, -# ) - -# def forward( -# self, x: torch.Tensor, fx: torch.Tensor = None, T: torch.Tensor = None -# ) -> torch.Tensor: -# """Forward pass. - -# Parameters -# ---------- -# x : torch.Tensor -# The input tensor. -# fx : torch.Tensor -# The function tensor. -# T : torch.Tensor -# The time tensor. - -# Returns -# ------- -# torch.Tensor -# The output tensor. - -# """ -# # print(f"Input x shape: {x.shape}") -# # print(f"Input fx shape: {fx.shape if fx is not None else None}") -# # print(f"Input T shape: {T.shape if T is not None else None}") -# y = self.model(x, fx, T) -# y = y.reshape(x.shape[0], self.H, self.W, -1) -# return y From 554a3aba052491c86998a3b44a6863084880d8e4 Mon Sep 17 00:00:00 2001 From: Corey Adams Date: Thu, 17 Jul 2025 07:16:44 -0700 Subject: [PATCH 09/23] Enable volume training in transolver. Still needs to be validated and optimized. --- examples/cfd/darcy_transolver/config_fix.yaml | 10 +- .../train_transolver_darcy_fix.py | 119 ++++-- .../transolver/README.md | 130 +++++++ .../conf/{train.yaml => train_surface.yaml} | 47 ++- .../transolver/conf/train_volume.yaml | 93 +++++ .../transolver/datapipe.py | 175 +++++++-- .../external_aerodynamics/transolver/loss.py | 162 +++++++- .../transolver/metrics.py | 46 ++- .../external_aerodynamics/transolver/train.py | 346 ++++++++++++++---- 9 files changed, 958 insertions(+), 170 deletions(-) rename examples/cfd/external_aerodynamics/transolver/conf/{train.yaml => train_surface.yaml} (73%) create mode 100644 examples/cfd/external_aerodynamics/transolver/conf/train_volume.yaml diff --git a/examples/cfd/darcy_transolver/config_fix.yaml b/examples/cfd/darcy_transolver/config_fix.yaml index e90fb50567..8f5cdcd2a0 100644 --- a/examples/cfd/darcy_transolver/config_fix.yaml +++ b/examples/cfd/darcy_transolver/config_fix.yaml @@ -21,12 +21,12 @@ # SOFTWARE. output_dir: ./output/darcy_transolver_fix -run_id: fp16_dev_fullres_b8_s64-te +run_id: bf16_dev_r85_b8_s64 data: train_path: /user_data/datasets/darcy_fix/example_data/piececonst_r421_N1024_smooth1.npz test_path: /user_data/datasets/darcy_fix/example_data/piececonst_r421_N1024_smooth2.npz - resolution: 421 #421, 211, 141, 106, 85 all viable + resolution: 85 #421, 211, 141, 106, 85 all viable batch_size: 8 # This is the GLOBAL batch size model: @@ -34,18 +34,18 @@ model: out_dim: 1 embedding_dim: 2 n_layers: 4 - n_hidden: 64 + n_hidden: 128 dropout: 0.0 n_head: 4 act: gelu - mlp_ratio: 2 + mlp_ratio: 4 unified_pos: False ref: 8 slice_num: 64 use_te: True Time_Input: False -precision: fp32 +precision: bf16 normaliser: permeability: diff --git a/examples/cfd/darcy_transolver/train_transolver_darcy_fix.py b/examples/cfd/darcy_transolver/train_transolver_darcy_fix.py index 2b6461b470..e915778fab 100644 --- a/examples/cfd/darcy_transolver/train_transolver_darcy_fix.py +++ b/examples/cfd/darcy_transolver/train_transolver_darcy_fix.py @@ -53,7 +53,16 @@ prof = Profiler() -def count_parameters(model): +def count_parameters(model: torch.nn.Module) -> int: + """ + Count the total number of trainable parameters in a PyTorch model. + + Args: + model (torch.nn.Module): The model whose parameters are to be counted. + + Returns: + int: Total number of trainable parameters. + """ total_params = 0 for name, parameter in model.named_parameters(): if not parameter.requires_grad: @@ -64,14 +73,35 @@ def count_parameters(model): def forward_train_full_loop( - model, loss_fun, optimizer, pos, x, y, y_normalizer, context, scaler=None -): + model: torch.nn.Module, + loss_fun: callable, + optimizer: torch.optim.Optimizer, + pos: torch.Tensor, + x: torch.Tensor, + y: torch.Tensor, + y_normalizer, + precision_context, + scaler: torch.cuda.amp.GradScaler = None, +) -> torch.Tensor: """ Forward and backward pass for one iteration, with optional mixed precision training. - Returns the loss for this minibatch + + Args: + model (torch.nn.Module): The model to train. + loss_fun (callable): Loss function. + optimizer (torch.optim.Optimizer): Optimizer. + pos (torch.Tensor): Position tensor (embedding). + x (torch.Tensor): Input tensor. + y (torch.Tensor): Target tensor. + y_normalizer: Normalizer for the target tensor. + precision_context: Context manager for precision (e.g., autocast). + scaler (torch.cuda.amp.GradScaler, optional): GradScaler for mixed precision. + + Returns: + torch.Tensor: The computed loss for this minibatch. """ dm = DistributedManager() - with context: + with precision_context: pred = model(embedding=pos, fx=x.unsqueeze(-1)).squeeze(-1) pred = y_normalizer.decode(pred) loss = loss_fun(pred, y) @@ -87,23 +117,43 @@ def forward_train_full_loop( def train_epoch( - model, - optimizer, - scheduler, - train_dataloader, - loss_fun, + model: torch.nn.Module, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler._LRScheduler, + train_dataloader: DataLoader, + loss_fun: callable, y_normalizer, - context, - scaler, -): + precision_context, + scaler: torch.cuda.amp.GradScaler, +) -> torch.Tensor: """ - One epoch of training. Returns the loss from the last minibatch used, averaged across replicas. - + One epoch of training. Returns the loss from the last minibatch used, averaged across replicas. + + Args: + model (torch.nn.Module): The model to train. + optimizer (torch.optim.Optimizer): Optimizer. + scheduler (torch.optim.lr_scheduler._LRScheduler): Learning rate scheduler. + train_dataloader (DataLoader): Training data loader. + loss_fun (callable): Loss function. + y_normalizer: Normalizer for the target tensor. + precision_context: Context manager for precision (e.g., autocast). + scaler (torch.cuda.amp.GradScaler): GradScaler for mixed precision. + + Returns: + torch.Tensor: The averaged loss from the last minibatch. """ for i, batch in enumerate(train_dataloader): pos, x, y = batch loss = forward_train_full_loop( - model, loss_fun, optimizer, pos, x, y, y_normalizer, context, scaler + model, + loss_fun, + optimizer, + pos, + x, + y, + y_normalizer, + precision_context, + scaler, ) scheduler.step() @@ -116,9 +166,27 @@ def train_epoch( return loss -def val_epoch(model, test_dataloader, loss_fun, y_normalizer): +def val_epoch( + model: torch.nn.Module, + test_dataloader: DataLoader, + loss_fun: callable, + y_normalizer, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: """ - One epoch of validation. Returns the loss averaged across the entire validation set. + One epoch of validation. Returns the loss averaged across the entire validation set. + + Args: + model (torch.nn.Module): The model to validate. + test_dataloader (DataLoader): Validation data loader. + loss_fun (callable): Loss function. + y_normalizer: Normalizer for the target tensor. + + Returns: + tuple: (val_loss, pred, y, RL2) + val_loss (torch.Tensor): Averaged validation loss. + pred (torch.Tensor): Last batch predictions. + y (torch.Tensor): Last batch targets. + RL2 (torch.Tensor): Averaged relative L2 error. """ val_loss = None RL2 = None @@ -159,7 +227,12 @@ def val_epoch(model, test_dataloader, loss_fun, y_normalizer): @hydra.main(version_base="1.3", config_path=".", config_name="config_fix.yaml") def darcy_trainer(cfg: DictConfig) -> None: - """Training for the 2D Darcy flow benchmark problem.""" + """ + Training entry point for the 2D Darcy flow benchmark problem. + + Args: + cfg (DictConfig): Configuration object loaded by Hydra. + """ ######################################################################## # Initialize distributed tools ######################################################################## @@ -301,13 +374,13 @@ def darcy_trainer(cfg: DictConfig) -> None: # Initialize GradScaler for mixed precision training if cfg.precision == "fp16": - context = torch.amp.autocast(device_type="cuda", dtype=torch.float16) + precision_context = torch.amp.autocast(device_type="cuda", dtype=torch.float16) scaler = torch.amp.GradScaler("cuda") elif cfg.precision == "bf16": - context = torch.amp.autocast(device_type="cuda", dtype=torch.bfloat16) + precision_context = torch.amp.autocast(device_type="cuda", dtype=torch.bfloat16) scaler = None else: - context = nullcontext() + precision_context = nullcontext() scaler = None if loaded_pseudo_epoch == 0: @@ -331,7 +404,7 @@ def darcy_trainer(cfg: DictConfig) -> None: train_dataloader, loss_fun, y_normalizer, - context, + precision_context, scaler, ) train_time = time.time() - train_start diff --git a/examples/cfd/external_aerodynamics/transolver/README.md b/examples/cfd/external_aerodynamics/transolver/README.md index e69de29bb2..363cc5f037 100644 --- a/examples/cfd/external_aerodynamics/transolver/README.md +++ b/examples/cfd/external_aerodynamics/transolver/README.md @@ -0,0 +1,130 @@ +# Transolver CFD Example: Code Overview + +This directory contains the core components for training and evaluating a Transolver +model for external aerodynamics CFD problems. The Transolver model is an adaptation +of the Attention mechanism to encourage models to learn meaningful representations. +In each PhysicsAttention layer, the input points are projected onto state vectors via +learnable transformations and weights. These learnable transformations are in turn +used to calculate self-attention between all state vectors, and the weights are reused +to project states back to each input point. + +Through a series of PhysicsAttention layers, the Transolver model learns high quality +projections from functional input space to output space. The PhysicsNeMo implementation +of Transolver is an equivalent implementation of the original model architecture +([https://github.com/thuml/Transolver](https://github.com/thuml/Transolver)), with +modifications to improve numerical stability and support NVIDIA TransformerEngine. + +The Transolver training recipe reuses the same input datasets as DoMINO - please see +the external_aerodynamics example for DoMINO for more information, as well as +[PhysicsNeMo Curator](https://github.com/NVIDIA/physicsnemo-curator) for information +on producing the data if needed. The Transolver model does not require the neighbor +points as input to the model, nor does it require any structure to the input points +(graph connections, edges, etc.). Consequently, the datapipe is significantly simpler +than for DoMINO - the entire datapipe is encapulated here in the examples. + +Below is a high-level overview of the main files and their roles in the workflow. + +--- + +## 1. `conf/train.yaml` + +**Purpose:** +Configuration file for training runs. +**Contents:** + +- **Output and run settings:** Output directory, run ID, random seed, and precision. +- **Training parameters:** Number of epochs, checkpoint intervals, and whether to use + compilation. +- **Model configuration:** Architecture details (input/output dimensions, number of + layers, embedding size, attention heads, activation, etc.). +- **Optimizer settings:** Type (AdamW), learning rate, weight decay, and optimizer + hyperparameters. +- **Data settings:** Paths to training/validation datasets, worker/thread settings, + memory pinning, and which data keys to load. +- **Logging:** Logging level and format. + +--- + +## 2. `src/train.py` + +**Purpose:** +Main training script for the DoMINO/Transolver model using distributed data parallelism. + +**Key Features:** + +- **Distributed Training:** Uses PyTorch DistributedDataParallel for multi-GPU training. +- **Data Loading:** Loads datasets using custom datapipe, supports distributed sampling. +- **Model Instantiation:** Builds the model based on config, supports both surface and + volume predictions. +- **Loss Calculation:** Computes losses for volume, surface, and integral quantities + (lift/drag). +- **Mixed Precision:** Supports mixed-precision training with gradient scaling. +- **Checkpointing:** Automatically loads/saves checkpoints and tracks best validation + loss. +- **Logging:** Logs metrics to TensorBoard and console, including GPU memory usage. +- **Validation:** Evaluates model on validation set after each epoch. + +--- + +## 3. `loss.py` + +**Purpose:** +Defines loss functions for training the Transolver model. + +**Key Features:** + +- **Surface Loss:** Computes MSE or RMSE for both pressure (scalar) and wall shear + (vector) components, handling them separately and combining the results. +- **Integral Losses:** Implements physics-based losses for lift and drag, using surface + integrals of predicted and true values, weighted by area, normals, and stream + velocity. +- **Modularity:** Loss functions are modular and can be combined or extended for + different training objectives. + +--- + +## 4. `metrics.py` + +**Purpose:** +Defines evaluation metrics for model predictions. + +**Key Features:** + +- **Distributed Reduction:** Aggregates metrics across distributed processes. +- **Surface Metrics:** Computes normalized L2 errors for pressure and shear components, + after unnormalizing predictions and targets. +- **Extensibility:** Can be extended for additional metrics or domains. + +--- + +## 5. `datapipe.py` + +**Purpose:** +Implements a PyTorch dataset for efficient loading of large CFD datasets stored in Zarr +format. + +**Key Features:** + +- **Chunk-Aligned I/O:** Reads large arrays in chunk-aligned fashion for efficiency, + using threads for parallel I/O. +- **Flexible Key Loading:** Allows specifying which data keys to load and which are + considered "large" (for chunked reading). +- **Pinned Memory:** Optionally allocates pinned memory for faster GPU transfers. +- **Prefetching:** Supports asynchronous preloading of samples to overlap I/O and + computation. + +--- + +## Summary + +- **`train.yaml`**: All configuration for model, data, optimizer, and logging. +- **`train.py`**: Orchestrates distributed training, validation, checkpointing, and + logging. +- **`loss.py`**: Physics-informed and standard loss functions for model training. +- **`metrics.py`**: Evaluation metrics for model performance, with distributed support. +- **`datapipe.py`**: High-performance, domain-parallel data loading from Zarr files for + large-scale CFD datasets. + +--- + +For more details, refer to the docstrings and comments within each file. diff --git a/examples/cfd/external_aerodynamics/transolver/conf/train.yaml b/examples/cfd/external_aerodynamics/transolver/conf/train_surface.yaml similarity index 73% rename from examples/cfd/external_aerodynamics/transolver/conf/train.yaml rename to examples/cfd/external_aerodynamics/transolver/conf/train_surface.yaml index 37dc470d9e..db56902f48 100644 --- a/examples/cfd/external_aerodynamics/transolver/conf/train.yaml +++ b/examples/cfd/external_aerodynamics/transolver/conf/train_surface.yaml @@ -16,15 +16,17 @@ -output_dir: "outputs" -run_id: "transolver_te-1cycle" +output_dir: "runs" +run_id: "surface/ReduceLR/No-TE/float16/" # Training configuration training: - num_epochs: 400 + precision: float16 # float32, float16, bfloat16 + num_epochs: 500 save_interval: 25 # Save checkpoint every N epochs seed: 42 - output_dir: "outputs" + output_dir: "runs" + compile: false # Model configuration model: @@ -42,15 +44,24 @@ model: unified_pos: false # Whether to use unified positional embeddings ref: 8 # Reference dimension for unified pos structured_shape: null - use_te: true # Use transformer engine + use_te: false # Use transformer engine Time_Input: false # Whether to use time embeddings +scheduler: + name: "OneCycleLR" + params: + final_div_factor: 1e4 + +# scheduler: +# name: "ReduceLROnPlateau" +# params: +# min_lr: 1e-6 # Optimizer configuration optimizer: _target_: torch.optim.AdamW - lr: 1.0e-3 + lr: 3.0e-4 weight_decay: 1.0e-4 betas: [0.9, 0.999] eps: 1.0e-8 @@ -58,25 +69,31 @@ optimizer: # Data configuration data: train: - data_path: /group_data/datasets/drivaer_aws/domino/train/ + data_path: /user_data/datasets/domino/train/ val: - data_path: /group_data/datasets/drivaer_aws/domino/val/ - max_workers: 32 + data_path: /user_data/datasets/domino/val/ + max_workers: 64 pin_memory: true - resolution: 262_144 - surface_keys: + # resolution: 262_144 + resolution: 325_000 + mode: surface + data_keys: - "surface_fields" - "surface_mesh_centers" - "surface_normals" - "air_density" - "stream_velocity" + - "stl_areas" + - "stl_centers" + large_keys: + - "surface_fields" + - "surface_mesh_centers" + - "surface_normals" + - "stl_areas" + - "stl_centers" # Logging configuration logging: level: INFO format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s' -# Hardware configuration -hardware: - precision: float32 - compile_model: false # Whether to use torch.compile \ No newline at end of file diff --git a/examples/cfd/external_aerodynamics/transolver/conf/train_volume.yaml b/examples/cfd/external_aerodynamics/transolver/conf/train_volume.yaml new file mode 100644 index 0000000000..889a57c0ff --- /dev/null +++ b/examples/cfd/external_aerodynamics/transolver/conf/train_volume.yaml @@ -0,0 +1,93 @@ +# SPDX-FileCopyrightText: Copyright (c) 2023 - 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-FileCopyrightText: All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + + +output_dir: "runs" +run_id: "volume/OneCycleLR/TE/float32/" + +# Training configuration +training: + precision: float32 # float32, float16, bfloat16 + num_epochs: 200 + save_interval: 25 # Save checkpoint every N epochs + seed: 42 + output_dir: "runs" + compile: false + +# Model configuration +model: + _target_: physicsnemo.models.transolver.Transolver + functional_dim: 2 # Input feature dimension + out_dim: 5 # Output feature dimension + embedding_dim: 3 # Spatial embedding dimension + n_layers: 8 # Number of transformer layers + n_hidden: 256 # Hidden dimension + dropout: 0.0 # Dropout rate + n_head: 8 # Number of attention heads + act: "gelu" # Activation function + mlp_ratio: 4 # MLP ratio in attention blocks + slice_num: 128 # Number of slices in physics attention + unified_pos: false # Whether to use unified positional embeddings + ref: 8 # Reference dimension for unified pos + structured_shape: null + use_te: true # Use transformer engine + Time_Input: false # Whether to use time embeddings + +scheduler: + name: "OneCycleLR" + params: + final_div_factor: 1e4 + +# scheduler: +# name: "ReduceLROnPlateau" +# params: +# min_lr: 1e-6 + + +# Optimizer configuration +optimizer: + _target_: torch.optim.AdamW + lr: 1.0e-3 + weight_decay: 1.0e-4 + betas: [0.9, 0.999] + eps: 1.0e-8 + +# Data configuration +data: + train: + data_path: /user_data/datasets/domino/train/ + val: + data_path: /user_data/datasets/domino/val/ + max_workers: 64 + pin_memory: true + # resolution: 262_144 + resolution: 200_000 + mode: volume + data_keys: + - "volume_mesh_centers" + - "volume_fields" + - "air_density" + - "stream_velocity" + large_keys: + - "volume_fields" + - "volume_mesh_centers" + +# Logging configuration +logging: + level: INFO + format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + diff --git a/examples/cfd/external_aerodynamics/transolver/datapipe.py b/examples/cfd/external_aerodynamics/transolver/datapipe.py index c1de86b431..1c9e168c01 100644 --- a/examples/cfd/external_aerodynamics/transolver/datapipe.py +++ b/examples/cfd/external_aerodynamics/transolver/datapipe.py @@ -54,14 +54,21 @@ from physicsnemo.distributed.utils import compute_split_shapes from physicsnemo.utils.profiling import profile +import threading + def get_filenames(data_path: Path, exclude_dirs: bool = True) -> List[str]: - """Get list of filenames from data directory.""" + """Get list of filenames from data directory. + + Args: + data_path: Path to the directory containing files. + exclude_dirs: If True, exclude directories from the result. + Returns: + Sorted list of filenames with .zarr extension. + """ filenames = [] for item in data_path.iterdir(): - # if exclude_dirs and item.is_dir(): - # continue if item.suffix in [".zarr"]: filenames.append(item.name) return sorted(filenames) @@ -72,8 +79,15 @@ def _read_chunk_into_array( zarr_array: zarr.Array, cpu_slice: slice, zarr_slice: slice = None, -): - """Helper function to read a chunk from zarr into numpy array.""" +) -> None: + """Helper function to read a chunk from zarr into numpy array. + + Args: + cpu_array: The destination numpy array. + zarr_array: The source zarr array. + cpu_slice: The slice in the cpu_array to write to. + zarr_slice: The slice in the zarr_array to read from. If None, uses cpu_slice. + """ if zarr_slice is None: zarr_slice = cpu_slice cpu_array[cpu_slice] = zarr_array[zarr_slice] @@ -81,7 +95,14 @@ def _read_chunk_into_array( @lru_cache def to_torch_dtype(dtype: np.dtype) -> torch.dtype: - """Convert a numpy dtype to a torch dtype.""" + """Convert a numpy dtype to a torch dtype. + + Args: + dtype: Numpy dtype to convert. + + Returns: + Corresponding torch dtype. + """ temp = torch.from_numpy(np.empty((), dtype=dtype)) return temp.dtype @@ -199,6 +220,7 @@ def __init__( self.tensor_specs = {} def __len__(self) -> int: + """Return the number of samples in the dataset.""" return len(self.filenames) def __del__(self): @@ -213,6 +235,14 @@ def _read_key_chunk_aligned( """ Read a key using chunk-aligned I/O for efficiency. Implements domain parallelism if device_mesh is configured. + + Args: + zarr_group: The zarr group to read from. + key: The key to read. + futures: List to append thread futures to. + + Returns: + Torch tensor containing the read data. """ zarr_array = zarr_group[key] @@ -325,7 +355,16 @@ def _read_key_chunk_aligned( def _read_key_standard( self, zarr_group: zarr.Group, key: str, futures: list[Future] ) -> torch.Tensor: - """Read a key with simple I/O (for small arrays).""" + """Read a key with simple I/O (for small arrays). + + Args: + zarr_group: The zarr group to read from. + key: The key to read. + futures: List to append thread futures to. + + Returns: + Torch tensor containing the read data. + """ zarr_array = zarr_group[key] # Handle scalar values @@ -376,7 +415,14 @@ def _read_key_standard( @profile def _read_zarr_file(self, filepath: Path) -> Dict[str, np.ndarray]: - """Read data from a zarr file.""" + """Read data from a zarr file. + + Args: + filepath: Path to the zarr file. + + Returns: + Dictionary mapping keys to numpy arrays or torch tensors. + """ with zarr.open_group(filepath, mode="r") as zarr_group: data = {} futures = [] @@ -402,7 +448,14 @@ def _read_zarr_file(self, filepath: Path) -> Dict[str, np.ndarray]: @profile def _move_to_gpu(self, data: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]: - """Convert numpy arrays to torch tensors and move to GPU if available.""" + """Convert numpy arrays to torch tensors and move to GPU if available. + + Args: + data: Dictionary of key to torch tensor. + + Returns: + Dictionary of key to torch tensor on GPU if available. + """ result = {} for key, array in data.items(): @@ -415,7 +468,14 @@ def _move_to_gpu(self, data: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor] def _convert_to_shard_tensors( self, tensors: Dict[str, torch.Tensor] ) -> Dict[str, Union[torch.Tensor, ShardTensor]]: - """Convert tensors to ShardTensor objects for distributed training.""" + """Convert tensors to ShardTensor objects for distributed training. + + Args: + tensors: Dictionary of key to torch tensor. + + Returns: + Dictionary of key to torch tensor or ShardTensor. + """ if self.device_mesh is None: return tensors @@ -446,6 +506,55 @@ def _convert_to_shard_tensors( return result + def preload(self, idx: int) -> None: + """ + Asynchronously preload the data for the given index (up to CPU, not GPU). + Only one preload operation is supported at a time. + + Args: + idx: Index of the sample to preload. + """ + if hasattr(self, "_preload_thread") and self._preload_thread is not None: + # Optionally, wait for previous preload to finish or raise error + self._preload_thread.join() + + self._preload_result = None + self._preload_exception = None + + def _preload_worker(): + try: + filename = self.filenames[idx] + filepath = self.data_path / filename + data = self._read_zarr_file(filepath) + self._preload_result = (idx, data) + except Exception as e: + self._preload_exception = e + + self._preload_thread = threading.Thread(target=_preload_worker) + self._preload_thread.start() + + def get_preloaded(self) -> Tuple[int, Dict[str, np.ndarray]]: + """ + Retrieve the preloaded data (blocking if not ready). + + Returns: + (idx, data) tuple where data is a dictionary of key to numpy array or torch tensor. + + Raises: + RuntimeError: If no preload is in progress. + Exception: If preload failed. + """ + if not hasattr(self, "_preload_thread") or self._preload_thread is None: + raise RuntimeError("No preload in progress. Call preload(idx) first.") + + self._preload_thread.join() + self._preload_thread = None + + if self._preload_exception is not None: + raise self._preload_exception + + return self._preload_result + @profile def __getitem__(self, idx: int) -> Dict[str, torch.Tensor | ShardTensor]: """ @@ -462,11 +571,26 @@ def __getitem__(self, idx: int) -> Dict[str, torch.Tensor | ShardTensor]: f"Index {idx} out of range for dataset of size {len(self.filenames)}" ) - filename = self.filenames[idx] - filepath = self.data_path / filename + if hasattr(self, "_preload_result") and self._preload_result is not None: + preload_idx, preload_data = self._preload_result + if preload_idx == idx: + data = preload_data + self._preload_result = None # Clear after use + else: + # Preloaded data is for a different idx, ignore it + data = self._read_zarr_file(self.data_path / self.filenames[idx]) + else: + filename = self.filenames[idx] + filepath = self.data_path / filename + + # Read data from zarr file + data = self._read_zarr_file(filepath) - # Read data from zarr file - data = self._read_zarr_file(filepath) + # filename = self.filenames[idx] + # filepath = self.data_path / filename + + # # Read data from zarr file + # data = self._read_zarr_file(filepath) # Convert to torch tensors tensors = self._move_to_gpu(data) @@ -485,26 +609,3 @@ def __getitem__(self, idx: int) -> Dict[str, torch.Tensor | ShardTensor]: # 4. Support for different placement strategies per key # 5. Memory-mapped reading for very large datasets # 6. Compression/decompression handling - - -def create_transolver_dataset( - data_path: Union[str, Path], - device_mesh: Optional[DeviceMesh] = None, - placements: Optional[Tuple[Placement, ...]] = None, - **kwargs, -) -> DomainParallelZarrDataset: - """ - Factory function to create a Transolver dataset with sensible defaults. - - Args: - data_path: Path to zarr data directory - device_mesh: Optional device mesh for domain parallelism - placements: Optional placement specifications - **kwargs: Additional arguments passed to DomainParallelZarrDataset - - Returns: - Configured dataset instance - """ - return DomainParallelZarrDataset( - data_path=data_path, device_mesh=device_mesh, placements=placements, **kwargs - ) diff --git a/examples/cfd/external_aerodynamics/transolver/loss.py b/examples/cfd/external_aerodynamics/transolver/loss.py index 3420d7324c..37dd31d969 100644 --- a/examples/cfd/external_aerodynamics/transolver/loss.py +++ b/examples/cfd/external_aerodynamics/transolver/loss.py @@ -18,8 +18,50 @@ from typing import Literal -def loss_fn(pred, target): - return loss_fn_surface(pred, target, "mse") +def loss_fn( + pred: torch.Tensor, + target: torch.Tensor, + others: dict[str, torch.Tensor], + mode: Literal["surface", "volume"], +) -> torch.Tensor: + """ + Compute the main loss function for the model. + + Args: + pred: Predicted tensor from the model. + target: Ground truth tensor. + others: Dictionary of additional tensors (e.g., surface_areas, surface_normals, stream_velocity). + + Returns: + Loss value as a scalar tensor. + """ + if mode == "surface": + loss = loss_fn_surface(pred, target, "mse") + elif mode == "volume": + loss = loss_fn_volume(pred, target, "rmse") + # 100 * integral_loss_fn(pred, target, others["surface_areas"], others["surface_normals"], others["stream_velocity"]) + return loss + + +def loss_fn_volume( + pred: torch.Tensor, target: torch.Tensor, mode: Literal["mse", "rmse"] +) -> torch.Tensor: + """ + Compute the main loss function for the model. + """ + + if mode == "rmse": + dims = (0, 1) + else: + dims = None + + num = torch.sum((pred - target) ** 2.0, dims) + if mode == "rmse": + denom = torch.sum(target**2.0, dims) + else: + denom = pred.shape[1] + + return torch.mean(num / denom) def loss_fn_surface( @@ -28,12 +70,12 @@ def loss_fn_surface( """Calculate loss for surface data by handling scalar and vector components separately. Args: - output: Predicted surface values from the model - target: Ground truth surface values - loss_type: Type of loss to calculate ("mse" or "rmse") + output: Predicted surface values from the model. + target: Ground truth surface values. + loss_type: Type of loss to calculate ("mse" or "rmse"). Returns: - Combined scalar and vector loss as a scalar tensor + Combined scalar and vector loss as a scalar tensor. """ # Separate the scalar and vector components: output_pressure, output_sheer = torch.split(output, [1, 3], dim=2) @@ -56,3 +98,111 @@ def loss_fn_surface( loss = loss_pressure + loss_wall_sheer return loss / 4.0 + + +def integral_loss_fn( + output: torch.Tensor, + target: torch.Tensor, + area: torch.Tensor, + normals: torch.Tensor, + stream_velocity: torch.Tensor = None, +) -> torch.Tensor: + """ + Compute the integral loss (sum of lift and drag losses). + + Args: + output: Predicted tensor. + target: Ground truth tensor. + area: Surface area tensor. + normals: Surface normals tensor. + stream_velocity: Stream velocity tensor (optional). + + Returns: + Scalar tensor representing the sum of lift and drag losses. + """ + drag_loss = drag_loss_fn(output, target, area, normals, stream_velocity) + lift_loss = lift_loss_fn(output, target, area, normals, stream_velocity) + return lift_loss + drag_loss + + +def lift_loss_fn( + output: torch.Tensor, + target: torch.Tensor, + area: torch.Tensor, + normals: torch.Tensor, + stream_velocity: torch.Tensor = None, +) -> torch.Tensor: + """ + Compute the lift loss. + + Args: + output: Predicted tensor. + target: Ground truth tensor. + area: Surface area tensor. + normals: Surface normals tensor. + stream_velocity: Stream velocity tensor (optional). + + Returns: + Scalar tensor representing the lift loss. + """ + vel_inlet = stream_velocity # Get this from the dataset + # mask = abs(target - padded_value) > 1e-3 + + output_true = target * area * (vel_inlet) ** 2.0 + output_pred = output * area * (vel_inlet) ** 2.0 + + normals = torch.select(normals, 2, 2) + output_true_0 = output_true.select(2, 0) + output_pred_0 = output_pred.select(2, 0) + + pres_true = output_true_0 * normals + pres_pred = output_pred_0 * normals + + wz_true = output_true[:, :, -1] + wz_pred = output_pred[:, :, -1] + + masked_pred = torch.mean(pres_pred + wz_pred, (1)) + masked_truth = torch.mean(pres_true + wz_true, (1)) + + loss = (masked_pred - masked_truth) ** 2.0 + loss = torch.mean(loss) + return loss + + +def drag_loss_fn( + output: torch.Tensor, + target: torch.Tensor, + area: torch.Tensor, + normals: torch.Tensor, + stream_velocity: torch.Tensor = None, +) -> torch.Tensor: + """ + Compute the drag loss. + + Args: + output: Predicted tensor. + target: Ground truth tensor. + area: Surface area tensor. + normals: Surface normals tensor. + stream_velocity: Stream velocity tensor (optional). + + Returns: + Scalar tensor representing the drag loss. + """ + vel_inlet = stream_velocity # Get this from the dataset + # mask = abs(target - padded_value) > 1e-3 + output_true = target * area * (vel_inlet) ** 2.0 + output_pred = output * area * (vel_inlet) ** 2.0 + + pres_true = output_true[:, :, 0] * normals[:, :, 0] + pres_pred = output_pred[:, :, 0] * normals[:, :, 0] + + wx_true = output_true[:, :, 1] + wx_pred = output_pred[:, :, 1] + + masked_pred = torch.mean(pres_pred + wx_pred, (1)) + masked_truth = torch.mean(pres_true + wx_true, (1)) + + loss = (masked_pred - masked_truth) ** 2.0 + loss = torch.mean(loss) + return loss diff --git a/examples/cfd/external_aerodynamics/transolver/metrics.py b/examples/cfd/external_aerodynamics/transolver/metrics.py index 5bba878e2a..2b3dfc1939 100644 --- a/examples/cfd/external_aerodynamics/transolver/metrics.py +++ b/examples/cfd/external_aerodynamics/transolver/metrics.py @@ -34,11 +34,45 @@ def all_reduce_dict(metrics, dm): return metrics -def metrics_fn(pred, target, dm): - return metrics_fn_surface(pred, target, dm) +def metrics_fn(pred, target, others, dm, mode): + if mode == "surface": + return metrics_fn_surface(pred, target, others, dm) + elif mode == "volume": + return metrics_fn_volume(pred, target, others, dm) + else: + raise ValueError(f"Unknown data mode: {mode}") -def metrics_fn_surface(pred, target, dm): +def metrics_fn_volume(pred, target, others, dm): + target = target * others["norm_std"] + others["norm_mean"] + pred = pred * others["norm_std"] + others["norm_mean"] + + l2_num = (pred - target) ** 2 + l2_num = torch.sum(l2_num, dim=1) + l2_num = torch.sqrt(l2_num) + + l2_denom = target**2 + l2_denom = torch.sum(l2_denom, dim=1) + l2_denom = torch.sqrt(l2_denom) + + l2 = l2_num / l2_denom + + metrics = { + "l2_volume_1": torch.mean(l2[:, 0]), + "l2_volume_2": torch.mean(l2[:, 1]), + "l2_volume_3": torch.mean(l2[:, 2]), + "l2_volume_4": torch.mean(l2[:, 3]), + "l2_volume_5": torch.mean(l2[:, 4]), + } + + return metrics + + +def metrics_fn_surface(pred, target, others, dm): + + # Unnormalize the surface values for L2: + target = target * others["norm_std"] + others["norm_mean"] + pred = pred * others["norm_std"] + others["norm_mean"] l2_num = (pred - target) ** 2 l2_num = torch.sum(l2_num, dim=1) @@ -52,9 +86,9 @@ def metrics_fn_surface(pred, target, dm): metrics = { "l2_pressure": torch.mean(l2[:, 0]), - "l2_sheer_x": torch.mean(l2[:, 1]), - "l2_sheer_y": torch.mean(l2[:, 2]), - "l2_sheer_z": torch.mean(l2[:, 3]), + "l2_shear_x": torch.mean(l2[:, 1]), + "l2_shear_y": torch.mean(l2[:, 2]), + "l2_shear_z": torch.mean(l2[:, 3]), } return metrics diff --git a/examples/cfd/external_aerodynamics/transolver/train.py b/examples/cfd/external_aerodynamics/transolver/train.py index 12b2e78708..26f348f04d 100644 --- a/examples/cfd/external_aerodynamics/transolver/train.py +++ b/examples/cfd/external_aerodynamics/transolver/train.py @@ -34,9 +34,60 @@ from loss import loss_fn from metrics import metrics_fn +from contextlib import nullcontext +from torch.amp import autocast, GradScaler + +# import transformer_engine.pytorch as te +# from transformer_engine.common.recipe import Format, DelayedScaling + + +def get_autocast_context(precision: str) -> nullcontext: + """ + Returns the appropriate autocast context for mixed precision training. + + Args: + precision (str): The desired precision. Supported values are "float16", "bfloat16", or any other string for no autocast. + + Returns: + Context manager: An autocast context for the specified precision, or a nullcontext if precision is not recognized. + """ + if precision == "float16": + return autocast("cuda", dtype=torch.float16) + elif precision == "bfloat16": + return autocast("cuda", dtype=torch.bfloat16) + # elif precision == "float8": + # print("Using float8 autocast") + # fp8_format = Format.HYBRID + # fp8_recipe = DelayedScaling(fp8_format=fp8_format, amax_history_len=16, amax_compute_algo="max") + # return te.fp8_autocast(enabled=True, fp8_recipe=fp8_recipe) + else: + return nullcontext() + + +def cast_precisions( + features: torch.Tensor, embeddings: torch.Tensor, precision: str +) -> tuple[torch.Tensor, torch.Tensor]: + """ + Casts the features and embeddings tensors to the specified precision. + + Args: + features (torch.Tensor): The input features tensor. + embeddings (torch.Tensor): The input embeddings tensor. + precision (str): The desired precision ("float16", "bfloat16", or other for no cast). + + Returns: + Tuple[torch.Tensor, torch.Tensor]: The features and embeddings tensors cast to the specified precision. + """ + if precision == "float16": + return features.to(torch.float16), embeddings.to(torch.float16) + elif precision == "bfloat16": + return features.to(torch.bfloat16), embeddings.to(torch.bfloat16) + else: + return features, embeddings + @profile -def preprocess_data(batch): +def preprocess_surface_data(batch): mesh_centers = batch["surface_mesh_centers"] normals = batch["surface_normals"] @@ -45,6 +96,10 @@ def preprocess_data(batch): [batch["air_density"], batch["stream_velocity"]], dim=-1 ).to(torch.float32) + # Normalize the surface fields: + norm_mean = targets.mean(dim=0) + norm_std = targets.std(dim=0) + targets = (targets - norm_mean) / norm_std # fourier_sin_features = [ # torch.sin(mesh_centers * (2 ** i) * torch.pi) # for i in range(4) @@ -54,6 +109,17 @@ def preprocess_data(batch): # for i in range(4) # ] + # Calculate center of mass + sizes = batch["stl_areas"] + centers = batch["stl_centers"] + + total_weighted_position = torch.einsum("i,ij->j", sizes, centers) + total_size = torch.sum(sizes) + center_of_mass = total_weighted_position[None, ...] / total_size + + # Subtract the COM from the centers: + mesh_centers = mesh_centers - center_of_mass + embeddings = torch.cat( [ mesh_centers, @@ -66,16 +132,70 @@ def preprocess_data(batch): node_features = node_features.unsqueeze(0).broadcast_to(embeddings.shape[0], -1) - return node_features, embeddings, targets + others = { + "surface_areas": sizes, + "surface_normals": normals, + "stream_velocity": batch["stream_velocity"], + "air_density": batch["air_density"], + "norm_mean": norm_mean, + "norm_std": norm_std, + } + + return node_features, embeddings, targets, others + + +def preprocess_volume_data(batch): + + mesh_centers = batch["volume_mesh_centers"] + targets = batch["volume_fields"] + node_features = torch.stack( + [batch["air_density"], batch["stream_velocity"]], dim=-1 + ).to(torch.float32) + + embeddings = mesh_centers + + # Normalize the surface fields: + norm_mean = targets.mean(dim=0) + norm_std = targets.std(dim=0) + targets = (targets - norm_mean) / norm_std + + node_features = node_features.unsqueeze(0).broadcast_to(embeddings.shape[0], -1) + + others = { + "norm_mean": norm_mean, + "norm_std": norm_std, + } + return node_features, embeddings, targets, others @profile -def downsample(features, embeddings, targets, num_keep=1024): +def downsample_surface(features, embeddings, targets, num_keep=1024): # Determine the number of samples to keep (e.g., 50% of original size) num_samples = features.shape[0] + # Generate random indices to keep (faster for large num_samples) + indices = torch.multinomial( + torch.ones(num_samples, device=features.device), num_keep, replacement=False + ) - # Generate random indices to keep - indices = torch.randperm(num_samples)[:num_keep] + # Use the same indices to downsample all tensors + downsampled_features = features[indices] + downsampled_embeddings = embeddings[indices] + downsampled_targets = targets[indices] + + downsampled_features = downsampled_features.unsqueeze(0) + downsampled_embeddings = downsampled_embeddings.unsqueeze(0) + downsampled_targets = downsampled_targets.unsqueeze(0) + + return downsampled_features, downsampled_embeddings, downsampled_targets + + +@profile +def downsample_volume(features, embeddings, targets, num_keep=1024): + # Determine the number of samples to keep (e.g., 50% of original size) + num_samples = features.shape[0] + # The volume data is so large, that we'll sample randints + # which will very rarely duplicate + indices = torch.randint(0, num_samples, (num_keep,), device=features.device) # Use the same indices to downsample all tensors downsampled_features = features[indices] @@ -101,6 +221,7 @@ def train_epoch( epoch, cfg, dist_manager, + scaler=None, # <-- Added scaler argument ): """Train for one epoch @@ -117,63 +238,84 @@ def train_epoch( epoch_indices = list(sampler) epoch_len = len(epoch_indices) - + precision = getattr(cfg.training, "precision", "float32") + context = get_autocast_context(precision) start_time = time.time() - for i, batch_idx in enumerate(epoch_indices): - batch = dataloader[batch_idx] - # Get data from batch - features, embeddings, targets = preprocess_data(batch) + with Profiler(): + for i, batch_idx in enumerate(epoch_indices): + batch = dataloader[batch_idx] + # Get data from batch + if cfg.data.mode == "surface": + features, embeddings, targets, others = preprocess_surface_data(batch) + features, embeddings, targets = downsample_surface( + features, embeddings, targets, cfg.data.resolution + ) + elif cfg.data.mode == "volume": + features, embeddings, targets, others = preprocess_volume_data(batch) + features, embeddings, targets = downsample_volume( + features, embeddings, targets, cfg.data.resolution + ) + else: + raise ValueError(f"Unknown data mode: {cfg.data.mode}") - features, embeddings, targets = downsample( - features, embeddings, targets, cfg.data.resolution - ) + # preload the next batch, if we're not on the last batch + if i < epoch_len - 1: + dataloader.preload(epoch_indices[i + 1]) - # Forward pass - outputs = model(features, embeddings) + # Cast precisions: + features, embeddings = cast_precisions(features, embeddings, precision) - metrics = metrics_fn(outputs, targets, dist_manager) + with context: + outputs = model(features, embeddings) + loss = loss_fn(outputs, targets, others, cfg.data.mode) - if i == 0: - total_metrics = metrics - else: - total_metrics = { - k: total_metrics[k] + metrics[k].item() for k in metrics.keys() - } + metrics = metrics_fn(outputs, targets, others, dist_manager, cfg.data.mode) - loss = loss_fn(outputs, targets) + optimizer.zero_grad() + if precision == "float16" and scaler is not None: + scaler.scale(loss).backward() + scaler.step(optimizer) + scaler.update() + else: + loss.backward() + optimizer.step() - # Backward pass - optimizer.zero_grad() - loss.backward() - optimizer.step() - scheduler.step() + if not isinstance(scheduler, torch.optim.lr_scheduler.ReduceLROnPlateau): + scheduler.step() - end_time = time.time() - duration = end_time - start_time - start_time = end_time + end_time = time.time() - images_per_second = 1 / duration + # Logging + total_loss += loss.item() - # Logging - total_loss += loss.item() + if i == 0: + total_metrics = metrics + else: + total_metrics = { + k: total_metrics[k] + metrics[k].item() for k in metrics.keys() + } - logger.info( - f"Epoch {epoch} [{i}/{epoch_len}] Loss: {loss.item():.6f} Duration: {duration:.2f}s" - ) - if dist_manager.rank == 0: - writer.add_scalar( - "batch/learning_rate", - optimizer.param_groups[0]["lr"], - i + epoch_len * epoch, - ) - writer.add_scalar("batch/loss", loss.item(), i + epoch_len * epoch) - writer.add_scalar( - "batch/throughpu_per_gpu", images_per_second, i + epoch_len * epoch + duration = end_time - start_time + start_time = end_time + images_per_second = 1 / duration + + logger.info( + f"Epoch {epoch} [{i}/{epoch_len}] Loss: {loss.item():.6f} Duration: {duration:.2f}s" ) - for metric_name, metric_value in metrics.items(): + if dist_manager.rank == 0: + writer.add_scalar( + "batch/learning_rate", + optimizer.param_groups[0]["lr"], + i + epoch_len * epoch, + ) + writer.add_scalar("batch/loss", loss.item(), i + epoch_len * epoch) writer.add_scalar( - f"batch/{metric_name}", metric_value, i + epoch_len * epoch + "batch/throughpu_per_gpu", images_per_second, i + epoch_len * epoch ) + for metric_name, metric_value in metrics.items(): + writer.add_scalar( + f"batch/{metric_name}", metric_value, i + epoch_len * epoch + ) avg_loss = total_loss / epoch_len avg_metrics = {k: v / epoch_len for k, v in total_metrics.items()} @@ -192,7 +334,16 @@ def train_epoch( @profile -def val_epoch(dataloader, sampler, model, logger, val_writer, epoch, cfg, dist_manager): +def val_epoch( + dataloader, + sampler, + model, + logger, + val_writer, + epoch, + cfg, + dist_manager, +): """Validation for one epoch Args: @@ -212,22 +363,32 @@ def val_epoch(dataloader, sampler, model, logger, val_writer, epoch, cfg, dist_m epoch_indices = list(sampler) epoch_len = len(epoch_indices) + precision = getattr(cfg.training, "precision", "float32") + context = get_autocast_context(precision) start_time = time.time() with torch.no_grad(): # Disable gradient computation for i, batch_idx in enumerate(epoch_indices): batch = dataloader[batch_idx] # Get data from batch - features, embeddings, targets = preprocess_data(batch) - - features, embeddings, targets = downsample( - features, embeddings, targets, cfg.data.resolution - ) + if cfg.data.mode == "surface": + features, embeddings, targets, others = preprocess_surface_data(batch) + features, embeddings, targets = downsample_surface( + features, embeddings, targets, cfg.data.resolution + ) + elif cfg.data.mode == "volume": + features, embeddings, targets, others = preprocess_volume_data(batch) + features, embeddings, targets = downsample_volume( + features, embeddings, targets, cfg.data.resolution + ) + else: + raise ValueError(f"Unknown data mode: {cfg.data.mode}") - # Forward pass - outputs = model(features, embeddings) + with context: + outputs = model(features, embeddings) + loss = loss_fn(outputs, targets, others, cfg.data.mode) - metrics = metrics_fn(outputs, targets, dist_manager) + metrics = metrics_fn(outputs, targets, others, dist_manager, cfg.data.mode) if i == 0: total_metrics = metrics @@ -236,15 +397,13 @@ def val_epoch(dataloader, sampler, model, logger, val_writer, epoch, cfg, dist_m k: total_metrics[k] + metrics[k].item() for k in metrics.keys() } - loss = loss_fn(outputs, targets) + # Logging + total_loss += loss.item() end_time = time.time() duration = end_time - start_time start_time = end_time - # Logging - total_loss += loss.item() - logger.info( f"Val [{i}/{epoch_len}] Loss: {loss.item():.6f} Duration: {duration:.2f}s" ) @@ -301,6 +460,7 @@ def main(cfg: DictConfig): # Set up model model = hydra.utils.instantiate(cfg.model) + model.to(dist_manager.device) if dist_manager.world_size > 1: @@ -324,8 +484,8 @@ def main(cfg: DictConfig): placements=placements, max_workers=cfg.data.max_workers, pin_memory=cfg.data.pin_memory, - keys_to_read=cfg.data.surface_keys, - large_keys=None, + keys_to_read=cfg.data.data_keys, + large_keys=cfg.data.large_keys, ) # Validation dataset @@ -335,8 +495,8 @@ def main(cfg: DictConfig): placements=placements, max_workers=cfg.data.max_workers, pin_memory=cfg.data.pin_memory, - keys_to_read=cfg.data.surface_keys, - large_keys=None, + keys_to_read=cfg.data.data_keys, + large_keys=cfg.data.large_keys, ) # Set up distributed samplers @@ -358,15 +518,29 @@ def main(cfg: DictConfig): # Set up optimizer and scheduler optimizer = hydra.utils.instantiate(cfg.optimizer, params=model.parameters()) - # Set up OneCycleLR learning rate scheduler - scheduler = torch.optim.lr_scheduler.OneCycleLR( - optimizer, - max_lr=cfg.optimizer.lr, - total_steps=len(list(train_sampler)) * cfg.training.num_epochs, - pct_start=0.3, - div_factor=25.0, - final_div_factor=1e4, - ) + + # Set up learning rate scheduler based on config + scheduler_cfg = cfg.scheduler + scheduler_name = scheduler_cfg.name + scheduler_params = dict(scheduler_cfg.params) + + if scheduler_name == "OneCycleLR": + scheduler_params.setdefault("max_lr", cfg.optimizer.lr) + # Dynamically compute total_steps + total_steps = len(list(train_sampler)) * cfg.training.num_epochs + scheduler_params["total_steps"] = total_steps + scheduler = torch.optim.lr_scheduler.OneCycleLR(optimizer, **scheduler_params) + elif scheduler_name == "ReduceLROnPlateau": + scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( + optimizer, **scheduler_params + ) + elif scheduler_name == "StepLR": + scheduler = torch.optim.lr_scheduler.StepLR(optimizer, **scheduler_params) + else: + raise ValueError(f"Unknown scheduler: {scheduler_name}") + + precision = getattr(cfg.training, "precision", "float32") + scaler = GradScaler() if precision == "float16" else None ckpt_args = { "path": f"{cfg.output_dir}/runs/{cfg.run_id}/checkpoints", @@ -376,6 +550,9 @@ def main(cfg: DictConfig): } loaded_epoch = load_checkpoint(device=dist_manager.device, **ckpt_args) + if cfg.training.compile: + model = torch.compile(model) + # Training loop logger.info("Starting training...") for epoch in range(loaded_epoch, cfg.training.num_epochs): @@ -383,6 +560,7 @@ def main(cfg: DictConfig): train_sampler.set_epoch(epoch) val_sampler.set_epoch(epoch) + start_time = time.time() # Training phase train_loss = train_epoch( train_dataset, @@ -395,8 +573,12 @@ def main(cfg: DictConfig): epoch, cfg, dist_manager, + scaler, ) + end_time = time.time() + train_duration = end_time - start_time + start_time = time.time() # Validation phase val_loss = val_epoch( val_dataset, @@ -408,16 +590,23 @@ def main(cfg: DictConfig): cfg, dist_manager, ) + end_time = time.time() + val_duration = end_time - start_time # Log epoch results logger.info( - f"Epoch [{epoch}/{cfg.training.num_epochs}] Train Loss: {train_loss:.6f} Val Loss: {val_loss:.6f}" + f"Epoch [{epoch}/{cfg.training.num_epochs}] Train Loss: {train_loss:.6f} [duration: {train_duration:.2f}s] Val Loss: {val_loss:.6f} [duration: {val_duration:.2f}s]" ) # save checkpoint if epoch % cfg.training.save_interval == 0 and dist_manager.rank == 0: save_checkpoint(**ckpt_args, epoch=epoch) + if scheduler_name == "ReduceLROnPlateau": + scheduler.step(val_loss) + else: + scheduler.step() + logger.info("Training completed!") @@ -428,11 +617,12 @@ def launch(cfg: DictConfig): Args: cfg: Hydra configuration object """ - # profiler = Profiler() + profiler = Profiler() + # profiler.enable("torch") # profiler.enable("line_profiler") - # profiler.initialize() + profiler.initialize() main(cfg) - # profiler.finalize() + profiler.finalize() if __name__ == "__main__": From 8c95135fe9619a00cca867a37e7fa456ae9083d9 Mon Sep 17 00:00:00 2001 From: Corey Adams Date: Fri, 18 Jul 2025 13:35:27 +0000 Subject: [PATCH 10/23] Updating transolver example further, and including more readme info --- .../transolver/README.md | 232 ++++++++++-------- .../transolver/conf/train_surface.yaml | 28 ++- .../transolver/conf/train_volume.yaml | 14 +- .../transolver/datapipe.py | 65 ++++- .../external_aerodynamics/transolver/train.py | 52 ++-- 5 files changed, 230 insertions(+), 161 deletions(-) diff --git a/examples/cfd/external_aerodynamics/transolver/README.md b/examples/cfd/external_aerodynamics/transolver/README.md index 363cc5f037..d366f34843 100644 --- a/examples/cfd/external_aerodynamics/transolver/README.md +++ b/examples/cfd/external_aerodynamics/transolver/README.md @@ -1,130 +1,162 @@ # Transolver CFD Example: Code Overview -This directory contains the core components for training and evaluating a Transolver -model for external aerodynamics CFD problems. The Transolver model is an adaptation -of the Attention mechanism to encourage models to learn meaningful representations. -In each PhysicsAttention layer, the input points are projected onto state vectors via -learnable transformations and weights. These learnable transformations are in turn -used to calculate self-attention between all state vectors, and the weights are reused -to project states back to each input point. - -Through a series of PhysicsAttention layers, the Transolver model learns high quality -projections from functional input space to output space. The PhysicsNeMo implementation -of Transolver is an equivalent implementation of the original model architecture -([https://github.com/thuml/Transolver](https://github.com/thuml/Transolver)), with -modifications to improve numerical stability and support NVIDIA TransformerEngine. - -The Transolver training recipe reuses the same input datasets as DoMINO - please see -the external_aerodynamics example for DoMINO for more information, as well as -[PhysicsNeMo Curator](https://github.com/NVIDIA/physicsnemo-curator) for information -on producing the data if needed. The Transolver model does not require the neighbor -points as input to the model, nor does it require any structure to the input points -(graph connections, edges, etc.). Consequently, the datapipe is significantly simpler -than for DoMINO - the entire datapipe is encapulated here in the examples. - -Below is a high-level overview of the main files and their roles in the workflow. +This directory contains the essential components for training and evaluating a +Transolver model tailored to external aerodynamics CFD problems. The Transolver model +adapts the Attention mechanism, encouraging the learning of meaningful representations. +In each PhysicsAttention layer, input points are projected onto state vectors through +learnable transformations and weights. These transformations are then used to compute +self-attention among all state vectors, and the same weights are reused to project +states back to each input point. + +By stacking multiple PhysicsAttention layers, the Transolver model learns to map from +the functional input space to the output space with high fidelity. The PhysicsNeMo +implementation closely follows the original Transolver architecture +([https://github.com/thuml/Transolver](https://github.com/thuml/Transolver)), but +introduces modifications for improved numerical stability and compatibility with NVIDIA +TransformerEngine. + +The training workflow for Transolver leverages the same input datasets as DoMINO. For +more information on the datasets, refer to the external_aerodynamics example for DoMINO +and the [PhysicsNeMo Curator](https://github.com/NVIDIA/physicsnemo-curator) for data +preparation guidance. Unlike DoMINO, Transolver does not require neighbor points or any +explicit structure (such as graph connections or edges) in the input data, resulting in +a much simpler datapipe that is fully encapsulated within these examples. + +Below, we provide a high-level overview of the main files and their roles in the +workflow. --- -## 1. `conf/train.yaml` - -**Purpose:** -Configuration file for training runs. -**Contents:** - -- **Output and run settings:** Output directory, run ID, random seed, and precision. -- **Training parameters:** Number of epochs, checkpoint intervals, and whether to use - compilation. -- **Model configuration:** Architecture details (input/output dimensions, number of - layers, embedding size, attention heads, activation, etc.). -- **Optimizer settings:** Type (AdamW), learning rate, weight decay, and optimizer - hyperparameters. -- **Data settings:** Paths to training/validation datasets, worker/thread settings, - memory pinning, and which data keys to load. -- **Logging:** Logging level and format. +## 1. `src/train.py` + +This script serves as the main entry point for training the DoMINO/Transolver model, +utilizing distributed data parallelism. The training loop processes the entire dataset, +computing a simple point-wise relative L2 loss for either surface or volume data, and +includes downsampling to handle the high native mesh resolutions. + +The script is designed for multi-GPU training using PyTorch’s DistributedDataParallel, +and it loads datasets through a custom datapipe that supports distributed sampling. +Model instantiation is flexible, supporting either surface or volume predictions +as specified in the configuration. + +The script supports mixed-precision training with gradient +scaling, and it manages checkpointing with the physicsnemo checkpointing utils. +Throughout training, metrics are logged to both TensorBoard and the console and +validation is performed after each epoch. + +The script can be launched on a single GPU with + +```bash +python train.py --config-name train_surface +``` + +or, for multi-GPU training, use `torchrun` or other distributed job launch tools. + +Example output for one epoch of the script, in an 8 GPU run, looks like: + +```default +[2025-07-17 14:27:36,040][training][INFO] - Epoch 47 [0/54] Loss: 0.117565 Duration: 0.78s +[2025-07-17 14:27:36,548][training][INFO] - Epoch 47 [1/54] Loss: 0.109625 Duration: 0.51s +[2025-07-17 14:27:37,048][training][INFO] - Epoch 47 [2/54] Loss: 0.122574 Duration: 0.50s +[2025-07-17 14:27:37,556][training][INFO] - Epoch 47 [3/54] Loss: 0.125667 Duration: 0.51s +[2025-07-17 14:27:38,063][training][INFO] - Epoch 47 [4/54] Loss: 0.101863 Duration: 0.51s +[2025-07-17 14:27:38,547][training][INFO] - Epoch 47 [5/54] Loss: 0.113324 Duration: 0.48s +[2025-07-17 14:27:39,054][training][INFO] - Epoch 47 [6/54] Loss: 0.115478 Duration: 0.51s +...[remove for brevity]... +[2025-07-17 14:28:00,662][training][INFO] - Epoch 47 [49/54] Loss: 0.107935 Duration: 0.49s +[2025-07-17 14:28:01,178][training][INFO] - Epoch 47 [50/54] Loss: 0.100087 Duration: 0.52s +[2025-07-17 14:28:01,723][training][INFO] - Epoch 47 [51/54] Loss: 0.097733 Duration: 0.55s +[2025-07-17 14:28:02,194][training][INFO] - Epoch 47 [52/54] Loss: 0.116489 Duration: 0.47s +[2025-07-17 14:28:02,605][training][INFO] - Epoch 47 [53/54] Loss: 0.104865 Duration: 0.41s + +Epoch 47 Average Metrics: ++-------------+---------------------+ +| Metric | Average Value | ++-------------+---------------------+ +| l2_pressure | 0.20262257754802704 | +| l2_shear_x | 0.2623567283153534 | +| l2_shear_y | 0.35603201389312744 | +| l2_shear_z | 0.38965049386024475 | ++-------------+---------------------+ + +[2025-07-17 14:28:02,834][training][INFO] - Val [0/6] Loss: 0.114801 Duration: 0.22s +[2025-07-17 14:28:03,074][training][INFO] - Val [1/6] Loss: 0.111632 Duration: 0.24s +[2025-07-17 14:28:03,309][training][INFO] - Val [2/6] Loss: 0.105342 Duration: 0.23s +[2025-07-17 14:28:03,537][training][INFO] - Val [3/6] Loss: 0.111033 Duration: 0.23s +[2025-07-17 14:28:03,735][training][INFO] - Val [4/6] Loss: 0.099963 Duration: 0.20s +[2025-07-17 14:28:03,903][training][INFO] - Val [5/6] Loss: 0.092340 Duration: 0.17s + +Epoch 47 Validation Average Metrics: ++-------------+---------------------+ +| Metric | Average Value | ++-------------+---------------------+ +| l2_pressure | 0.19346082210540771 | +| l2_shear_x | 0.26041051745414734 | +| l2_shear_y | 0.3589216470718384 | +| l2_shear_z | 0.370105117559433 | ++-------------+---------------------+ +``` --- -## 2. `src/train.py` - -**Purpose:** -Main training script for the DoMINO/Transolver model using distributed data parallelism. +## 2. `conf/train_volume.yaml` and `conf/train_surface.yaml` -**Key Features:** - -- **Distributed Training:** Uses PyTorch DistributedDataParallel for multi-GPU training. -- **Data Loading:** Loads datasets using custom datapipe, supports distributed sampling. -- **Model Instantiation:** Builds the model based on config, supports both surface and - volume predictions. -- **Loss Calculation:** Computes losses for volume, surface, and integral quantities - (lift/drag). -- **Mixed Precision:** Supports mixed-precision training with gradient scaling. -- **Checkpointing:** Automatically loads/saves checkpoints and tracks best validation - loss. -- **Logging:** Logs metrics to TensorBoard and console, including GPU memory usage. -- **Validation:** Evaluates model on validation set after each epoch. +These configuration files define all the settings required for a training run. They +specify output directories, run identifiers, random seeds, and precision settings, as +well as the number of epochs, checkpoint intervals, and whether to use compilation. The +model architecture is described here, including input and output dimensions, the number +of layers, embedding size, attention heads, and activation functions. Optimizer +settings such as the type (AdamW), learning rate, weight decay, and other +hyperparameters are also included. Data-related settings cover paths to the training and +validation datasets, worker and thread counts, memory pinning, and which data keys to +load. Finally, logging preferences are set, controlling the level and format of output. --- ## 3. `loss.py` -**Purpose:** -Defines loss functions for training the Transolver model. - -**Key Features:** - -- **Surface Loss:** Computes MSE or RMSE for both pressure (scalar) and wall shear - (vector) components, handling them separately and combining the results. -- **Integral Losses:** Implements physics-based losses for lift and drag, using surface - integrals of predicted and true values, weighted by area, normals, and stream - velocity. -- **Modularity:** Loss functions are modular and can be combined or extended for - different training objectives. +This file defines the loss functions used during Transolver training, primarily +focusing on a relative L2 loss. For surface data, it computes mean squared error (MSE) +or root mean squared error (RMSE) for both pressure (a scalar) and wall shear (a +vector), handling each component separately before combining the results. The file also +implements physics-based losses for lift and drag, using surface integrals of predicted +and true values, weighted appropriately by area, normals, and stream velocity. The loss +functions are designed to be modular, allowing for easy combination or extension to +support different training objectives. --- ## 4. `metrics.py` -**Purpose:** -Defines evaluation metrics for model predictions. - -**Key Features:** - -- **Distributed Reduction:** Aggregates metrics across distributed processes. -- **Surface Metrics:** Computes normalized L2 errors for pressure and shear components, - after unnormalizing predictions and targets. -- **Extensibility:** Can be extended for additional metrics or domains. +Evaluation metrics for model predictions are defined here. The metrics are designed to +aggregate results across distributed processes, ensuring consistency in multi-GPU +setups. For surface predictions, the script computes normalized L2 errors for both +pressure and shear components, after unnormalizing the predictions and targets. The +structure of the code allows for straightforward extension to additional metrics or +application domains as needed. --- ## 5. `datapipe.py` -**Purpose:** -Implements a PyTorch dataset for efficient loading of large CFD datasets stored in Zarr -format. - -**Key Features:** - -- **Chunk-Aligned I/O:** Reads large arrays in chunk-aligned fashion for efficiency, - using threads for parallel I/O. -- **Flexible Key Loading:** Allows specifying which data keys to load and which are - considered "large" (for chunked reading). -- **Pinned Memory:** Optionally allocates pinned memory for faster GPU transfers. -- **Prefetching:** Supports asynchronous preloading of samples to overlap I/O and - computation. +Efficient loading of large CFD datasets stored in Zarr format is handled by this file, +which implements a PyTorch dataset. The datapipe reads large arrays in a chunk-aligned +manner for optimal performance, leveraging threads for parallel I/O. It offers +flexibility in specifying which data keys to load and which are considered “large” (and +thus read in chunks). For faster GPU transfers, it can allocate pinned memory, and it +supports asynchronous prefetching of samples to overlap I/O with computation, further +improving throughput. --- ## Summary -- **`train.yaml`**: All configuration for model, data, optimizer, and logging. -- **`train.py`**: Orchestrates distributed training, validation, checkpointing, and - logging. -- **`loss.py`**: Physics-informed and standard loss functions for model training. -- **`metrics.py`**: Evaluation metrics for model performance, with distributed support. -- **`datapipe.py`**: High-performance, domain-parallel data loading from Zarr files for - large-scale CFD datasets. - ---- +In summary, the configuration files (`train.yaml`) centralize all settings for the +model, data, optimizer, and logging. The main training script (`train.py`) orchestrates +distributed training, validation, checkpointing, and logging. Loss functions in +`loss.py` combine physics-informed and standard approaches, while `metrics.py` provides +robust evaluation metrics with distributed support. Finally, `datapipe.py` ensures +high-performance, domain-parallel data loading from Zarr files, enabling large-scale +CFD training. -For more details, refer to the docstrings and comments within each file. +For further details, consult the docstrings and comments within each file. diff --git a/examples/cfd/external_aerodynamics/transolver/conf/train_surface.yaml b/examples/cfd/external_aerodynamics/transolver/conf/train_surface.yaml index db56902f48..3c01ef57fe 100644 --- a/examples/cfd/external_aerodynamics/transolver/conf/train_surface.yaml +++ b/examples/cfd/external_aerodynamics/transolver/conf/train_surface.yaml @@ -17,7 +17,7 @@ output_dir: "runs" -run_id: "surface/ReduceLR/No-TE/float16/" +run_id: "surface/ReduceLR/TE/float16/" # Training configuration training: @@ -44,24 +44,26 @@ model: unified_pos: false # Whether to use unified positional embeddings ref: 8 # Reference dimension for unified pos structured_shape: null - use_te: false # Use transformer engine + use_te: true # Use transformer engine Time_Input: false # Whether to use time embeddings -scheduler: - name: "OneCycleLR" - params: - final_div_factor: 1e4 - # scheduler: -# name: "ReduceLROnPlateau" +# name: "OneCycleLR" # params: -# min_lr: 1e-6 +# final_div_factor: 1e4 + +# StepLR scheduler: Decays the learning rate by gamma every step_size epochs +scheduler: + name: "StepLR" + params: + step_size: 100 # Decay every 100 epochs (set X as desired) + gamma: 0.5 # Decay factor # Optimizer configuration optimizer: _target_: torch.optim.AdamW - lr: 3.0e-4 + lr: 1.0e-3 weight_decay: 1.0e-4 betas: [0.9, 0.999] eps: 1.0e-8 @@ -69,12 +71,12 @@ optimizer: # Data configuration data: train: - data_path: /user_data/datasets/domino/train/ + data_path: /group_data/datasets/drivaer_aws/domino/train/ val: - data_path: /user_data/datasets/domino/val/ + data_path: /group_data/datasets/drivaer_aws/domino/val/ max_workers: 64 pin_memory: true - # resolution: 262_144 + # resolution: 10000 resolution: 325_000 mode: surface data_keys: diff --git a/examples/cfd/external_aerodynamics/transolver/conf/train_volume.yaml b/examples/cfd/external_aerodynamics/transolver/conf/train_volume.yaml index 889a57c0ff..f3bd7275d2 100644 --- a/examples/cfd/external_aerodynamics/transolver/conf/train_volume.yaml +++ b/examples/cfd/external_aerodynamics/transolver/conf/train_volume.yaml @@ -17,12 +17,12 @@ output_dir: "runs" -run_id: "volume/OneCycleLR/TE/float32/" +run_id: "volume/OneCycleLR/noTE/float32/" # Training configuration training: precision: float32 # float32, float16, bfloat16 - num_epochs: 200 + num_epochs: 1 save_interval: 25 # Save checkpoint every N epochs seed: 42 output_dir: "runs" @@ -44,7 +44,7 @@ model: unified_pos: false # Whether to use unified positional embeddings ref: 8 # Reference dimension for unified pos structured_shape: null - use_te: true # Use transformer engine + use_te: false # Use transformer engine Time_Input: false # Whether to use time embeddings scheduler: @@ -69,10 +69,10 @@ optimizer: # Data configuration data: train: - data_path: /user_data/datasets/domino/train/ + data_path: /group_data/datasets/drivaer_aws/domino/train/ val: - data_path: /user_data/datasets/domino/val/ - max_workers: 64 + data_path: /group_data/datasets/drivaer_aws/domino/val/ + max_workers: 8 pin_memory: true # resolution: 262_144 resolution: 200_000 @@ -82,7 +82,7 @@ data: - "volume_fields" - "air_density" - "stream_velocity" - large_keys: + large_keys: - "volume_fields" - "volume_mesh_centers" diff --git a/examples/cfd/external_aerodynamics/transolver/datapipe.py b/examples/cfd/external_aerodynamics/transolver/datapipe.py index 1c9e168c01..6d42b349f6 100644 --- a/examples/cfd/external_aerodynamics/transolver/datapipe.py +++ b/examples/cfd/external_aerodynamics/transolver/datapipe.py @@ -175,6 +175,9 @@ def __init__( self.keys_to_read = keys_to_read or set() self.large_keys = large_keys or set() + self.data_loader_stream = torch.cuda.Stream() + self.gpu_transfer_compete = None + # Validate distributed configuration if self.device_mesh is not None: if self.device_mesh.ndim != 1: @@ -458,10 +461,25 @@ def _move_to_gpu(self, data: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor] """ result = {} - for key, array in data.items(): - # Move to GPU if available - if self.dm.cuda: - result[key] = data[key].to(self.dm.device, non_blocking=True) + # This is old code. + # Thinking about ways to reduce CPU overhead. + # If we could avoid the data reading step, it would be easier. + # total_size = 0 + # for key, array in data.items(): + # size = array.numel() * array.element_size() + # total_size += size + # print(f"Size in memory of {key}: {size / 1024 / 1024 / 1024} GB") + # print(f"Total size in memory: {total_size / 1024 / 1024 / 1024} GB") + + with torch.cuda.stream(self.data_loader_stream): + + for key, array in data.items(): + # Move to GPU if available + if self.dm.cuda: + result[key] = data[key].to(self.dm.device, non_blocking=True) + + self.gpu_transfer_compete = torch.cuda.Event() + self.gpu_transfer_compete.record(self.data_loader_stream) return result @@ -526,6 +544,8 @@ def _preload_worker(): filename = self.filenames[idx] filepath = self.data_path / filename data = self._read_zarr_file(filepath) + # Convert to torch tensors + data = self._move_to_gpu(data) self._preload_result = (idx, data) except Exception as e: self._preload_exception = e @@ -555,6 +575,25 @@ def get_preloaded(self) -> Tuple[int, Dict[str, np.ndarray]]: return self._preload_result + def __iter__(self): + self.i = 0 + return self + + def __next__(self): + """ + This supports the DALI iterator. + """ + if self.i >= len(self.filenames): + self.i = 0 + raise StopIteration + data = self._read_zarr_file(self.data_path / self.filenames[self.i]) + + self.i += 1 + return tuple(data[key].unsqueeze(0) for key in self.keys_to_read) + + def __len__(self): + return len(self.filenames) + @profile def __getitem__(self, idx: int) -> Dict[str, torch.Tensor | ShardTensor]: """ @@ -579,27 +618,27 @@ def __getitem__(self, idx: int) -> Dict[str, torch.Tensor | ShardTensor]: else: # Preloaded data is for a different idx, ignore it data = self._read_zarr_file(self.data_path / self.filenames[idx]) + data = self._move_to_gpu(data) else: filename = self.filenames[idx] filepath = self.data_path / filename # Read data from zarr file data = self._read_zarr_file(filepath) + data = self._move_to_gpu(data) - # filename = self.filenames[idx] - # filepath = self.data_path / filename - - # # Read data from zarr file - # data = self._read_zarr_file(filepath) + # This blocks until the preprocessing has transferred to GPU + if self.gpu_transfer_compete is not None: + torch.cuda.current_stream().wait_event(self.gpu_transfer_compete) - # Convert to torch tensors - tensors = self._move_to_gpu(data) + # Add a batch index: + data = {key: value.unsqueeze(0) for key, value in data.items()} # Convert to ShardTensors if using domain parallelism if self.device_mesh is not None: - tensors = self._convert_to_shard_tensors(tensors) + data = self._convert_to_shard_tensors(data) - return tensors + return data # TODO: Additional features to consider implementing: diff --git a/examples/cfd/external_aerodynamics/transolver/train.py b/examples/cfd/external_aerodynamics/transolver/train.py index 26f348f04d..4626171493 100644 --- a/examples/cfd/external_aerodynamics/transolver/train.py +++ b/examples/cfd/external_aerodynamics/transolver/train.py @@ -97,8 +97,8 @@ def preprocess_surface_data(batch): ).to(torch.float32) # Normalize the surface fields: - norm_mean = targets.mean(dim=0) - norm_std = targets.std(dim=0) + norm_mean = targets.mean(dim=1) + norm_std = targets.std(dim=1) targets = (targets - norm_mean) / norm_std # fourier_sin_features = [ # torch.sin(mesh_centers * (2 ** i) * torch.pi) @@ -113,7 +113,7 @@ def preprocess_surface_data(batch): sizes = batch["stl_areas"] centers = batch["stl_centers"] - total_weighted_position = torch.einsum("i,ij->j", sizes, centers) + total_weighted_position = torch.einsum("ki,kij->kj", sizes, centers) total_size = torch.sum(sizes) center_of_mass = total_weighted_position[None, ...] / total_size @@ -129,8 +129,7 @@ def preprocess_surface_data(batch): ], dim=-1, ) - - node_features = node_features.unsqueeze(0).broadcast_to(embeddings.shape[0], -1) + node_features = node_features.unsqueeze(1).broadcast_to(1, embeddings.shape[1], -1) others = { "surface_areas": sizes, @@ -171,20 +170,20 @@ def preprocess_volume_data(batch): @profile def downsample_surface(features, embeddings, targets, num_keep=1024): # Determine the number of samples to keep (e.g., 50% of original size) - num_samples = features.shape[0] + num_samples = features.shape[1] # Generate random indices to keep (faster for large num_samples) indices = torch.multinomial( torch.ones(num_samples, device=features.device), num_keep, replacement=False ) # Use the same indices to downsample all tensors - downsampled_features = features[indices] - downsampled_embeddings = embeddings[indices] - downsampled_targets = targets[indices] + downsampled_features = features[:, indices] + downsampled_embeddings = embeddings[:, indices] + downsampled_targets = targets[:, indices] - downsampled_features = downsampled_features.unsqueeze(0) - downsampled_embeddings = downsampled_embeddings.unsqueeze(0) - downsampled_targets = downsampled_targets.unsqueeze(0) + # downsampled_features = downsampled_features.unsqueeze(0) + # downsampled_embeddings = downsampled_embeddings.unsqueeze(0) + # downsampled_targets = downsampled_targets.unsqueeze(0) return downsampled_features, downsampled_embeddings, downsampled_targets @@ -192,19 +191,15 @@ def downsample_surface(features, embeddings, targets, num_keep=1024): @profile def downsample_volume(features, embeddings, targets, num_keep=1024): # Determine the number of samples to keep (e.g., 50% of original size) - num_samples = features.shape[0] + num_samples = features.shape[1] # The volume data is so large, that we'll sample randints # which will very rarely duplicate indices = torch.randint(0, num_samples, (num_keep,), device=features.device) # Use the same indices to downsample all tensors - downsampled_features = features[indices] - downsampled_embeddings = embeddings[indices] - downsampled_targets = targets[indices] - - downsampled_features = downsampled_features.unsqueeze(0) - downsampled_embeddings = downsampled_embeddings.unsqueeze(0) - downsampled_targets = downsampled_targets.unsqueeze(0) + downsampled_features = features[:, indices] + downsampled_embeddings = embeddings[:, indices] + downsampled_targets = targets[:, indices] return downsampled_features, downsampled_embeddings, downsampled_targets @@ -236,13 +231,14 @@ def train_epoch( total_loss = 0 total_metrics = {} - epoch_indices = list(sampler) + epoch_indices = list(sampler) if sampler is not None else range(len(dataloader)) epoch_len = len(epoch_indices) precision = getattr(cfg.training, "precision", "float32") context = get_autocast_context(precision) start_time = time.time() with Profiler(): for i, batch_idx in enumerate(epoch_indices): + batch = dataloader[batch_idx] # Get data from batch if cfg.data.mode == "surface": @@ -259,7 +255,7 @@ def train_epoch( raise ValueError(f"Unknown data mode: {cfg.data.mode}") # preload the next batch, if we're not on the last batch - if i < epoch_len - 1: + if i < epoch_len - 1 and sampler is not None: dataloader.preload(epoch_indices[i + 1]) # Cast precisions: @@ -280,7 +276,7 @@ def train_epoch( loss.backward() optimizer.step() - if not isinstance(scheduler, torch.optim.lr_scheduler.ReduceLROnPlateau): + if not isinstance(scheduler, torch.optim.lr_scheduler.StepLR): scheduler.step() end_time = time.time() @@ -361,7 +357,7 @@ def val_epoch( total_loss = 0 total_metrics = {} - epoch_indices = list(sampler) + epoch_indices = list(sampler) if sampler is not None else range(len(dataloader)) epoch_len = len(epoch_indices) precision = getattr(cfg.training, "precision", "float32") context = get_autocast_context(precision) @@ -478,6 +474,7 @@ def main(cfg: DictConfig): placements = None # Training dataset + train_dataset = DomainParallelZarrDataset( data_path=cfg.data.train.data_path, device_mesh=device_mesh, @@ -489,6 +486,7 @@ def main(cfg: DictConfig): ) # Validation dataset + val_dataset = DomainParallelZarrDataset( data_path=cfg.data.val.data_path, # Assuming validation data path is configured device_mesh=device_mesh, @@ -543,7 +541,7 @@ def main(cfg: DictConfig): scaler = GradScaler() if precision == "float16" else None ckpt_args = { - "path": f"{cfg.output_dir}/runs/{cfg.run_id}/checkpoints", + "path": f"{cfg.output_dir}/{cfg.run_id}/checkpoints", "optimizer": optimizer, "scheduler": scheduler, "models": model, @@ -602,9 +600,7 @@ def main(cfg: DictConfig): if epoch % cfg.training.save_interval == 0 and dist_manager.rank == 0: save_checkpoint(**ckpt_args, epoch=epoch) - if scheduler_name == "ReduceLROnPlateau": - scheduler.step(val_loss) - else: + if scheduler_name == "StepLR": scheduler.step() logger.info("Training completed!") From 858c5371a1aead05505e72b7c58fb55711c99adb Mon Sep 17 00:00:00 2001 From: Corey Adams Date: Fri, 18 Jul 2025 13:12:37 -0700 Subject: [PATCH 11/23] Further integrate transformer engine into Transolver. --- .../models/transolver/Physics_Attention.py | 19 +++++-- physicsnemo/models/transolver/transolver.py | 51 +++++++++++++------ 2 files changed, 50 insertions(+), 20 deletions(-) diff --git a/physicsnemo/models/transolver/Physics_Attention.py b/physicsnemo/models/transolver/Physics_Attention.py index ab1409eb21..556ea28ec3 100644 --- a/physicsnemo/models/transolver/Physics_Attention.py +++ b/physicsnemo/models/transolver/Physics_Attention.py @@ -63,8 +63,13 @@ def __init__(self, dim, heads, dim_head, dropout, slice_num, use_te): self.softmax = nn.Softmax(dim=-1) self.dropout = nn.Dropout(dropout) self.temperature = nn.Parameter(torch.ones([1, heads, 1, 1]) * 0.5) + self.use_te = use_te + + if self.use_te: + self.in_project_slice = te.Linear(dim_head, slice_num) + else: + self.in_project_slice = nn.Linear(dim_head, slice_num) - self.in_project_slice = nn.Linear(dim_head, slice_num) for l_i in [self.in_project_slice]: torch.nn.init.orthogonal_(l_i.weight) # use a principled initialization if not use_te: @@ -73,7 +78,7 @@ def __init__(self, dim, heads, dim_head, dropout, slice_num, use_te): self.to_v = nn.Linear(dim_head, dim_head, bias=False) else: # These are used in the transformer engine pass function: - self.qkv_project = nn.Linear(dim_head, 3 * dim_head, bias=False) + self.qkv_project = te.Linear(dim_head, 3 * dim_head, bias=False) self.attn_fn = te.DotProductAttention( num_attention_heads=self.heads, kv_channels=self.dim_head, @@ -82,9 +87,12 @@ def __init__(self, dim, heads, dim_head, dropout, slice_num, use_te): softmax_scale=self.scale, ) - self.use_te = use_te + if self.use_te: + self.out_linear = te.Linear(inner_dim, dim) + else: + self.out_linear = nn.Linear(inner_dim, dim) - self.to_out = nn.Sequential(nn.Linear(inner_dim, dim), nn.Dropout(dropout)) + self.out_dropout = nn.Dropout(dropout) def project_input_onto_slices(self, x) -> tuple[torch.Tensor, torch.Tensor]: """ @@ -216,7 +224,8 @@ def project_attention_outputs( out_x = torch.matmul(slice_weights, out_slice_token) out_x = rearrange(out_x, "b h n d -> b n (h d)") - return self.to_out(out_x) + out_x = self.out_linear(out_x) + return self.out_dropout(out_x) def forward(self, x: torch.Tensor) -> torch.Tensor: """ diff --git a/physicsnemo/models/transolver/transolver.py b/physicsnemo/models/transolver/transolver.py index 6dbf16d3f7..51b953a2cb 100644 --- a/physicsnemo/models/transolver/transolver.py +++ b/physicsnemo/models/transolver/transolver.py @@ -72,7 +72,9 @@ class MLP(nn.Module): - def __init__(self, n_input, n_hidden, n_output, n_layers=1, act="gelu", res=True): + def __init__( + self, n_input, n_hidden, n_output, n_layers=1, act="gelu", res=True, use_te=True + ): super(MLP, self).__init__() if act in ACTIVATION.keys(): @@ -84,22 +86,27 @@ def __init__(self, n_input, n_hidden, n_output, n_layers=1, act="gelu", res=True self.n_output = n_output self.n_layers = n_layers self.res = res - self.linear_pre = nn.Sequential(nn.Linear(n_input, n_hidden), act()) - self.linear_post = nn.Linear(n_hidden, n_output) - self.linears = nn.ModuleList( - [ - nn.Sequential(nn.Linear(n_hidden, n_hidden), act()) - for _ in range(n_layers) - ] - ) + + self.act = act() + + linear_layer = nn.Linear if not use_te else te.Linear + + self.linear_pre = linear_layer(n_input, n_hidden) + self.linear_post = linear_layer(n_hidden, n_output) + # self.linears = nn.ModuleList( + # [ + # nn.Sequential(linear_layer(n_hidden, n_hidden), act()) + # for _ in range(n_layers) + # ] + # ) def forward(self, x): - x = self.linear_pre(x) - for i in range(self.n_layers): - if self.res: - x = self.linears[i](x) + x - else: - x = self.linears[i](x) + x = self.act(self.linear_pre(x)) + # for i in range(self.n_layers): + # if self.res: + # x = self.linears[i](x) + x + # else: + # x = self.linears[i](x) x = self.linear_post(x) return x @@ -173,6 +180,18 @@ def __init__( hidden_size=hidden_dim, ffn_hidden_size=hidden_dim * mlp_ratio, ) + # self.ln_mlp1 = nn.Sequential( + # te.LayerNorm(hidden_dim), + # MLP( + # hidden_dim, + # hidden_dim * mlp_ratio, + # hidden_dim, + # n_layers=0, + # res=False, + # act=act, + # use_te=True + # ), + # ) else: self.ln_mlp1 = nn.Sequential( nn.LayerNorm(hidden_dim), @@ -183,6 +202,7 @@ def __init__( n_layers=0, res=False, act=act, + use_te=False, ), ) if self.last_layer: @@ -377,6 +397,7 @@ def __init__( n_layers=0, res=False, act=act, + use_te=False, ) self.Time_Input = Time_Input From 221cab11768e6fa1db9d283c8f966bd65217f85f Mon Sep 17 00:00:00 2001 From: Corey Adams Date: Fri, 18 Jul 2025 13:56:45 -0700 Subject: [PATCH 12/23] Update readme to point out matlab to npz conversion of fixed dataset. --- examples/cfd/darcy_transolver/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/cfd/darcy_transolver/README.md b/examples/cfd/darcy_transolver/README.md index 571f19bdc3..fcfa96da19 100644 --- a/examples/cfd/darcy_transolver/README.md +++ b/examples/cfd/darcy_transolver/README.md @@ -44,6 +44,10 @@ and the data path should be added when `Darcy_2D_fix` dataset is constructed. You can download the data [here](https://huggingface.co/datasets/lkuang/example_data). +The `fix` dataset training (which uses a fixed dataset) requires you to convert +data from matlab to numpy format, for faster startup of the training. Just +use the `convert_mat_to_npz.py` script to port your data. + ## Model overview and architecture ## Getting Started From 5ea778909b7b45b8534cd21871f1c82f6a31b6ce Mon Sep 17 00:00:00 2001 From: Corey Adams Date: Fri, 18 Jul 2025 14:51:20 -0700 Subject: [PATCH 13/23] Update README and improve scripts --- .../transolver/README.md | 38 +-- .../transolver/conf/train_surface.yaml | 12 +- .../transolver/conf/train_volume.yaml | 15 +- .../transolver/metrics.py | 114 ++++++- .../transolver/requirements.txt | 6 + .../external_aerodynamics/transolver/train.py | 292 ++++++++++++------ 6 files changed, 323 insertions(+), 154 deletions(-) create mode 100644 examples/cfd/external_aerodynamics/transolver/requirements.txt diff --git a/examples/cfd/external_aerodynamics/transolver/README.md b/examples/cfd/external_aerodynamics/transolver/README.md index d366f34843..80b6a00474 100644 --- a/examples/cfd/external_aerodynamics/transolver/README.md +++ b/examples/cfd/external_aerodynamics/transolver/README.md @@ -111,6 +111,9 @@ hyperparameters are also included. Data-related settings cover paths to the trai validation datasets, worker and thread counts, memory pinning, and which data keys to load. Finally, logging preferences are set, controlling the level and format of output. +Note that, to use TransformerEngine, you must enable it directly in the model settings. +TransformerEngine is incompatible with `torch.compile`. + --- ## 3. `loss.py` @@ -118,11 +121,7 @@ load. Finally, logging preferences are set, controlling the level and format of This file defines the loss functions used during Transolver training, primarily focusing on a relative L2 loss. For surface data, it computes mean squared error (MSE) or root mean squared error (RMSE) for both pressure (a scalar) and wall shear (a -vector), handling each component separately before combining the results. The file also -implements physics-based losses for lift and drag, using surface integrals of predicted -and true values, weighted appropriately by area, normals, and stream velocity. The loss -functions are designed to be modular, allowing for easy combination or extension to -support different training objectives. +vector), handling each component separately before combining the results. --- @@ -140,23 +139,16 @@ application domains as needed. ## 5. `datapipe.py` Efficient loading of large CFD datasets stored in Zarr format is handled by this file, -which implements a PyTorch dataset. The datapipe reads large arrays in a chunk-aligned +which implements like a PyTorch dataset. The datapipe reads large arrays in a chunk-aligned manner for optimal performance, leveraging threads for parallel I/O. It offers flexibility in specifying which data keys to load and which are considered “large” (and -thus read in chunks). For faster GPU transfers, it can allocate pinned memory, and it -supports asynchronous prefetching of samples to overlap I/O with computation, further -improving throughput. - ---- - -## Summary - -In summary, the configuration files (`train.yaml`) centralize all settings for the -model, data, optimizer, and logging. The main training script (`train.py`) orchestrates -distributed training, validation, checkpointing, and logging. Loss functions in -`loss.py` combine physics-informed and standard approaches, while `metrics.py` provides -robust evaluation metrics with distributed support. Finally, `datapipe.py` ensures -high-performance, domain-parallel data loading from Zarr files, enabling large-scale -CFD training. - -For further details, consult the docstrings and comments within each file. +thus read in chunks, vs. small arrays which are directly read in their entirety). +For faster GPU transfers, it can allocate directly to pinned memory, then share buffers +with numpy for streaming from disk directly to pinned memory. The dataload has a +prefetch utility, as well, enabling the dataloader to queue the next batch to GPU. + +CPU to GPU transfers are performed in a separate CUDA stream from the main computation, +enabling async transfers overlapping with model training. The main parameter to tune +is the number of workers for the threading: too many, and you will introduce +CPU overhead limiting the model performance. Too few, and the dataload won't acheive +peak throughput of dataloading. diff --git a/examples/cfd/external_aerodynamics/transolver/conf/train_surface.yaml b/examples/cfd/external_aerodynamics/transolver/conf/train_surface.yaml index 3c01ef57fe..76b9c6e691 100644 --- a/examples/cfd/external_aerodynamics/transolver/conf/train_surface.yaml +++ b/examples/cfd/external_aerodynamics/transolver/conf/train_surface.yaml @@ -17,7 +17,7 @@ output_dir: "runs" -run_id: "surface/ReduceLR/TE/float16/" +run_id: "surface/ReduceLR/TE/float16-3/" # Training configuration training: @@ -44,7 +44,7 @@ model: unified_pos: false # Whether to use unified positional embeddings ref: 8 # Reference dimension for unified pos structured_shape: null - use_te: true # Use transformer engine + use_te: True # Use transformer engine Time_Input: false # Whether to use time embeddings # scheduler: @@ -56,14 +56,14 @@ model: scheduler: name: "StepLR" params: - step_size: 100 # Decay every 100 epochs (set X as desired) + step_size: 50 # Decay every 100 epochs (set X as desired) gamma: 0.5 # Decay factor # Optimizer configuration optimizer: _target_: torch.optim.AdamW - lr: 1.0e-3 + lr: 3.0e-4 weight_decay: 1.0e-4 betas: [0.9, 0.999] eps: 1.0e-8 @@ -71,9 +71,9 @@ optimizer: # Data configuration data: train: - data_path: /group_data/datasets/drivaer_aws/domino/train/ + data_path: /user_data/datasets/domino/train/ val: - data_path: /group_data/datasets/drivaer_aws/domino/val/ + data_path: /user_data/datasets/domino/val/ max_workers: 64 pin_memory: true # resolution: 10000 diff --git a/examples/cfd/external_aerodynamics/transolver/conf/train_volume.yaml b/examples/cfd/external_aerodynamics/transolver/conf/train_volume.yaml index f3bd7275d2..97f24b9134 100644 --- a/examples/cfd/external_aerodynamics/transolver/conf/train_volume.yaml +++ b/examples/cfd/external_aerodynamics/transolver/conf/train_volume.yaml @@ -17,16 +17,16 @@ output_dir: "runs" -run_id: "volume/OneCycleLR/noTE/float32/" +run_id: "volume/OneCycleLR/noTE/bfloat16/" # Training configuration training: - precision: float32 # float32, float16, bfloat16 - num_epochs: 1 + precision: bfloat16 # float32, float16, bfloat16 + num_epochs: 250 save_interval: 25 # Save checkpoint every N epochs seed: 42 output_dir: "runs" - compile: false + compile: true # Model configuration model: @@ -69,12 +69,11 @@ optimizer: # Data configuration data: train: - data_path: /group_data/datasets/drivaer_aws/domino/train/ + data_path: /user_data/datasets/domino/train/ val: - data_path: /group_data/datasets/drivaer_aws/domino/val/ - max_workers: 8 + data_path: /user_data/datasets/domino/val/ + max_workers: 32 pin_memory: true - # resolution: 262_144 resolution: 200_000 mode: volume data_keys: diff --git a/examples/cfd/external_aerodynamics/transolver/metrics.py b/examples/cfd/external_aerodynamics/transolver/metrics.py index 2b3dfc1939..18e8ecfa2e 100644 --- a/examples/cfd/external_aerodynamics/transolver/metrics.py +++ b/examples/cfd/external_aerodynamics/transolver/metrics.py @@ -20,7 +20,19 @@ from physicsnemo.distributed import DistributedManager -def all_reduce_dict(metrics, dm): +def all_reduce_dict( + metrics: dict[str, torch.Tensor], dm: DistributedManager +) -> dict[str, torch.Tensor]: + """ + Reduces a dictionary of metrics across all distributed processes. + + Args: + metrics: Dictionary of metric names to torch.Tensor values. + dm: DistributedManager instance for distributed context. + + Returns: + Dictionary of reduced metrics. + """ # TODO - update this to use domains and not the full world if dm.world_size == 1: @@ -34,16 +46,56 @@ def all_reduce_dict(metrics, dm): return metrics -def metrics_fn(pred, target, others, dm, mode): - if mode == "surface": - return metrics_fn_surface(pred, target, others, dm) - elif mode == "volume": - return metrics_fn_volume(pred, target, others, dm) - else: - raise ValueError(f"Unknown data mode: {mode}") +def metrics_fn( + pred: torch.Tensor, + target: torch.Tensor, + others: dict[str, torch.Tensor], + dm: DistributedManager, + mode: str, +) -> dict[str, torch.Tensor]: + """ + Computes metrics for either surface or volume data. + + Args: + pred: Predicted values (unnormalized). + target: Target values (unnormalized). + others: Dictionary containing normalization statistics. + dm: DistributedManager instance for distributed context. + mode: Either "surface" or "volume". + + Returns: + Dictionary of computed metrics. + """ + with torch.no_grad(): + if mode == "surface": + metrics = metrics_fn_surface(pred, target, others, dm) + elif mode == "volume": + metrics = metrics_fn_volume(pred, target, others, dm) + else: + raise ValueError(f"Unknown data mode: {mode}") + + metrics = all_reduce_dict(metrics, dm) + return metrics -def metrics_fn_volume(pred, target, others, dm): +def metrics_fn_volume( + pred: torch.Tensor, + target: torch.Tensor, + others: dict[str, torch.Tensor], + dm: DistributedManager, +) -> dict[str, torch.Tensor]: + """ + Computes L2 volume metrics between prediction and target. + + Args: + pred: Predicted values (normalized). + target: Target values (normalized). + others: Dictionary containing normalization statistics. + dm: DistributedManager instance for distributed context. + + Returns: + Dictionary of L2 volume metrics. + """ target = target * others["norm_std"] + others["norm_mean"] pred = pred * others["norm_std"] + others["norm_mean"] @@ -58,18 +110,34 @@ def metrics_fn_volume(pred, target, others, dm): l2 = l2_num / l2_denom metrics = { - "l2_volume_1": torch.mean(l2[:, 0]), - "l2_volume_2": torch.mean(l2[:, 1]), - "l2_volume_3": torch.mean(l2[:, 2]), - "l2_volume_4": torch.mean(l2[:, 3]), - "l2_volume_5": torch.mean(l2[:, 4]), + "lr_volume_pressure": torch.mean(l2[:, 0]), + "l2_velocity_x": torch.mean(l2[:, 1]), + "l2_velocity_y": torch.mean(l2[:, 2]), + "l2_velocity_z": torch.mean(l2[:, 3]), + "l2_turb_visc": torch.mean(l2[:, 4]), } return metrics -def metrics_fn_surface(pred, target, others, dm): - +def metrics_fn_surface( + pred: torch.Tensor, + target: torch.Tensor, + others: dict[str, torch.Tensor], + dm: DistributedManager, +) -> dict[str, torch.Tensor]: + """ + Computes L2 surface metrics between prediction and target. + + Args: + pred: Predicted values (normalized). + target: Target values (normalized). + others: Dictionary containing normalization statistics. + dm: DistributedManager instance for distributed context. + + Returns: + Dictionary of L2 surface metrics. + """ # Unnormalize the surface values for L2: target = target * others["norm_std"] + others["norm_mean"] pred = pred * others["norm_std"] + others["norm_mean"] @@ -94,5 +162,17 @@ def metrics_fn_surface(pred, target, others, dm): return metrics -def metrics_fn_surface_pressure(pred, target): +def metrics_fn_surface_pressure( + pred: torch.Tensor, target: torch.Tensor +) -> torch.Tensor: + """ + Computes mean squared error between predicted and target surface pressure. + + Args: + pred: Predicted surface pressure. + target: Target surface pressure. + + Returns: + Mean squared error as a torch.Tensor. + """ return torch.mean((pred - target) ** 2.0) diff --git a/examples/cfd/external_aerodynamics/transolver/requirements.txt b/examples/cfd/external_aerodynamics/transolver/requirements.txt new file mode 100644 index 0000000000..e0414bda88 --- /dev/null +++ b/examples/cfd/external_aerodynamics/transolver/requirements.txt @@ -0,0 +1,6 @@ +hydra-core +tabulate +tensorboard +termcolor +einops +transformer_engine[pytorch] \ No newline at end of file diff --git a/examples/cfd/external_aerodynamics/transolver/train.py b/examples/cfd/external_aerodynamics/transolver/train.py index 4626171493..954dd42a2d 100644 --- a/examples/cfd/external_aerodynamics/transolver/train.py +++ b/examples/cfd/external_aerodynamics/transolver/train.py @@ -37,8 +37,8 @@ from contextlib import nullcontext from torch.amp import autocast, GradScaler -# import transformer_engine.pytorch as te -# from transformer_engine.common.recipe import Format, DelayedScaling +import transformer_engine.pytorch as te +from transformer_engine.common.recipe import Format, DelayedScaling def get_autocast_context(precision: str) -> nullcontext: @@ -55,11 +55,12 @@ def get_autocast_context(precision: str) -> nullcontext: return autocast("cuda", dtype=torch.float16) elif precision == "bfloat16": return autocast("cuda", dtype=torch.bfloat16) - # elif precision == "float8": - # print("Using float8 autocast") - # fp8_format = Format.HYBRID - # fp8_recipe = DelayedScaling(fp8_format=fp8_format, amax_history_len=16, amax_compute_algo="max") - # return te.fp8_autocast(enabled=True, fp8_recipe=fp8_recipe) + elif precision == "float8": + fp8_format = Format.HYBRID + fp8_recipe = DelayedScaling( + fp8_format=fp8_format, amax_history_len=16, amax_compute_algo="max" + ) + return te.fp8_autocast(enabled=True, fp8_recipe=fp8_recipe) else: return nullcontext() @@ -87,7 +88,17 @@ def cast_precisions( @profile -def preprocess_surface_data(batch): +def preprocess_surface_data( + batch: dict, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, dict]: + + """ + Preprocess the surface data. The functional input + is the air density and stream velocity. The embeddings + are the surface mesh centers and normals. The targets are + normalized to mean of 0, std 1. We cache the mean and std + to de-normalize when computing the metrics. + """ mesh_centers = batch["surface_mesh_centers"] normals = batch["surface_normals"] @@ -100,6 +111,10 @@ def preprocess_surface_data(batch): norm_mean = targets.mean(dim=1) norm_std = targets.std(dim=1) targets = (targets - norm_mean) / norm_std + + # If you want to use this, be sure to updat the + # functional_dim value in your configuration + # fourier_sin_features = [ # torch.sin(mesh_centers * (2 ** i) * torch.pi) # for i in range(4) @@ -143,7 +158,16 @@ def preprocess_surface_data(batch): return node_features, embeddings, targets, others -def preprocess_volume_data(batch): +def preprocess_volume_data( + batch: dict, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, dict]: + """ + Preprocess the volumetric data. Right now, it's just + normalizing the targets and using the mesh centers as embeddings. + + The targets are normalized to mean of 0, std 1. We cache the + mean and std to de-normalize when computing the metrics. + """ mesh_centers = batch["volume_mesh_centers"] targets = batch["volume_fields"] @@ -154,11 +178,11 @@ def preprocess_volume_data(batch): embeddings = mesh_centers # Normalize the surface fields: - norm_mean = targets.mean(dim=0) - norm_std = targets.std(dim=0) + norm_mean = targets.mean(dim=1) + norm_std = targets.std(dim=1) targets = (targets - norm_mean) / norm_std - node_features = node_features.unsqueeze(0).broadcast_to(embeddings.shape[0], -1) + node_features = node_features.unsqueeze(0).broadcast_to(1, embeddings.shape[1], -1) others = { "norm_mean": norm_mean, @@ -168,7 +192,17 @@ def preprocess_volume_data(batch): @profile -def downsample_surface(features, embeddings, targets, num_keep=1024): +def downsample_surface( + features: torch.Tensor, + embeddings: torch.Tensor, + targets: torch.Tensor, + num_keep=1024, +): + """ + Downsample the surface data. We generate one set of indices, and + use it to sample the same points from the features, embeddings, + and targets. Using torch.multinomial to sample without replacement. + """ # Determine the number of samples to keep (e.g., 50% of original size) num_samples = features.shape[1] # Generate random indices to keep (faster for large num_samples) @@ -181,15 +215,23 @@ def downsample_surface(features, embeddings, targets, num_keep=1024): downsampled_embeddings = embeddings[:, indices] downsampled_targets = targets[:, indices] - # downsampled_features = downsampled_features.unsqueeze(0) - # downsampled_embeddings = downsampled_embeddings.unsqueeze(0) - # downsampled_targets = downsampled_targets.unsqueeze(0) - return downsampled_features, downsampled_embeddings, downsampled_targets @profile -def downsample_volume(features, embeddings, targets, num_keep=1024): +def downsample_volume( + features: torch.Tensor, + embeddings: torch.Tensor, + targets: torch.Tensor, + num_keep=1024, +): + """ + Downsample the volume data. torch.multinomial has a limit of 2^24 + for num_samples, and the volumetric data typically exceeds that. + + So, this isjust sampling randomly with num_keep. The hope + is that the duplication is small ... but this needs to be refined. + """ # Determine the number of samples to keep (e.g., 50% of original size) num_samples = features.shape[1] # The volume data is so large, that we'll sample randints @@ -204,28 +246,81 @@ def downsample_volume(features, embeddings, targets, num_keep=1024): return downsampled_features, downsampled_embeddings, downsampled_targets +def forward_pass( + batch: dict, + model: torch.nn.Module, + precision: str, + output_pad_size: int | None, + dist_manager: DistributedManager, + cfg: DictConfig, +): + """ + Run the forward pass of the model for one batch, including metrics and loss calculation. + """ + + if cfg.data.mode == "surface": + features, embeddings, targets, others = preprocess_surface_data(batch) + features, embeddings, targets = downsample_surface( + features, embeddings, targets, cfg.data.resolution + ) + + elif cfg.data.mode == "volume": + features, embeddings, targets, others = preprocess_volume_data(batch) + features, embeddings, targets = downsample_volume( + features, embeddings, targets, cfg.data.resolution + ) + else: + raise ValueError(f"Unknown data mode: {cfg.data.mode}") + + # Cast precisions: + features, embeddings = cast_precisions(features, embeddings, precision) + + with get_autocast_context(precision): + outputs = model(features, embeddings) + if output_pad_size is not None: + # Remove the padded outputs: + outputs = outputs[:, :, :-output_pad_size] + loss = loss_fn(outputs, targets, others, cfg.data.mode) + + metrics = metrics_fn(outputs, targets, others, dist_manager, cfg.data.mode) + + return loss, metrics + + @profile def train_epoch( dataloader, - sampler, - model, - optimizer, - scheduler, - logger, - writer, - epoch, - cfg, - dist_manager, - scaler=None, # <-- Added scaler argument -): - """Train for one epoch + sampler: torch.utils.data.Sampler | None, + model: torch.nn.Module, + output_pad_size: int | None, + optimizer: torch.optim.Optimizer, + scheduler: torch.optim.lr_scheduler._LRScheduler, + logger: PythonLogger, + writer: SummaryWriter, + epoch: int, + cfg: DictConfig, + dist_manager: DistributedManager, + scaler: GradScaler | None = None, +) -> float: + """ + Train the model for one epoch. Args: - dataloader: Training data loader - model: The model to train - logger: Python logger instance - writer: Tensorboard writer - cfg: Configuration object + dataloader (list[dict]): Training data loader + sampler (torch.utils.data.Sampler | None): Sampler for distributed or sequential sampling. + model (torch.nn.Module): The neural network model to train. + output_pad_size (int | None): Optional output padding size for lowest precisions (FP8). + optimizer (torch.optim.Optimizer): Optimizer for model parameters. + scheduler (torch.optim.lr_scheduler._LRScheduler): Learning rate scheduler. + logger (PythonLogger): Logger for training progress. + writer (SummaryWriter): TensorBoard writer for logging metrics. + epoch (int): Current epoch number. + cfg (DictConfig): Hydra configuration object. + dist_manager (DistributedManager): Distributed manager from physicsnemo. + scaler (GradScaler | None, optional): Gradient scaler for mixed precision training. + + Returns: + float: The average training loss for the epoch. """ model.train() total_loss = 0 @@ -234,38 +329,17 @@ def train_epoch( epoch_indices = list(sampler) if sampler is not None else range(len(dataloader)) epoch_len = len(epoch_indices) precision = getattr(cfg.training, "precision", "float32") - context = get_autocast_context(precision) start_time = time.time() with Profiler(): for i, batch_idx in enumerate(epoch_indices): - batch = dataloader[batch_idx] - # Get data from batch - if cfg.data.mode == "surface": - features, embeddings, targets, others = preprocess_surface_data(batch) - features, embeddings, targets = downsample_surface( - features, embeddings, targets, cfg.data.resolution - ) - elif cfg.data.mode == "volume": - features, embeddings, targets, others = preprocess_volume_data(batch) - features, embeddings, targets = downsample_volume( - features, embeddings, targets, cfg.data.resolution - ) - else: - raise ValueError(f"Unknown data mode: {cfg.data.mode}") - # preload the next batch, if we're not on the last batch if i < epoch_len - 1 and sampler is not None: dataloader.preload(epoch_indices[i + 1]) - # Cast precisions: - features, embeddings = cast_precisions(features, embeddings, precision) - - with context: - outputs = model(features, embeddings) - loss = loss_fn(outputs, targets, others, cfg.data.mode) - - metrics = metrics_fn(outputs, targets, others, dist_manager, cfg.data.mode) + loss, metrics = forward_pass( + batch, model, precision, output_pad_size, dist_manager, cfg + ) optimizer.zero_grad() if precision == "float16" and scaler is not None: @@ -282,7 +356,8 @@ def train_epoch( end_time = time.time() # Logging - total_loss += loss.item() + this_loss = loss.detach().item() + total_loss += this_loss if i == 0: total_metrics = metrics @@ -296,7 +371,7 @@ def train_epoch( images_per_second = 1 / duration logger.info( - f"Epoch {epoch} [{i}/{epoch_len}] Loss: {loss.item():.6f} Duration: {duration:.2f}s" + f"Epoch {epoch} [{i}/{epoch_len}] Loss: {this_loss:.6f} Duration: {duration:.2f}s" ) if dist_manager.rank == 0: writer.add_scalar( @@ -304,7 +379,7 @@ def train_epoch( optimizer.param_groups[0]["lr"], i + epoch_len * epoch, ) - writer.add_scalar("batch/loss", loss.item(), i + epoch_len * epoch) + writer.add_scalar("batch/loss", this_loss, i + epoch_len * epoch) writer.add_scalar( "batch/throughpu_per_gpu", images_per_second, i + epoch_len * epoch ) @@ -332,25 +407,31 @@ def train_epoch( @profile def val_epoch( dataloader, - sampler, - model, - logger, - val_writer, - epoch, - cfg, - dist_manager, -): - """Validation for one epoch + sampler: torch.utils.data.Sampler | None, + model: torch.nn.Module, + output_pad_size: int | None, + logger: PythonLogger, + val_writer: SummaryWriter, + epoch: int, + cfg: DictConfig, + dist_manager: DistributedManager, +) -> float: + """ + Run validation for one epoch. Args: - dataloader: Validation data loader - sampler: Validation data sampler - model: The model to evaluate - logger: Python logger instance - writer: Tensorboard writer - epoch: Current epoch number - cfg: Configuration object - dist_manager: Distributed manager instance + dataloader (list[dict]): Validation data loader. + sampler (torch.utils.data.Sampler | None): Sampler for distributed or sequential sampling. + model (torch.nn.Module): The model to evaluate. + output_pad_size (int | None): Optional output padding size for lowest precisions (FP8). + logger (PythonLogger): Logger for validation progress. + val_writer (SummaryWriter): TensorBoard writer for logging validation metrics. + epoch (int): Current epoch number. + cfg (DictConfig): Hydra configuration object. + dist_manager (DistributedManager): Distributed manager instance. + + Returns: + float: The average validation loss for the epoch. """ model.eval() # Set model to evaluation mode @@ -360,31 +441,20 @@ def val_epoch( epoch_indices = list(sampler) if sampler is not None else range(len(dataloader)) epoch_len = len(epoch_indices) precision = getattr(cfg.training, "precision", "float32") - context = get_autocast_context(precision) start_time = time.time() with torch.no_grad(): # Disable gradient computation for i, batch_idx in enumerate(epoch_indices): - batch = dataloader[batch_idx] # Get data from batch - if cfg.data.mode == "surface": - features, embeddings, targets, others = preprocess_surface_data(batch) - features, embeddings, targets = downsample_surface( - features, embeddings, targets, cfg.data.resolution - ) - elif cfg.data.mode == "volume": - features, embeddings, targets, others = preprocess_volume_data(batch) - features, embeddings, targets = downsample_volume( - features, embeddings, targets, cfg.data.resolution - ) - else: - raise ValueError(f"Unknown data mode: {cfg.data.mode}") + batch = dataloader[batch_idx] - with context: - outputs = model(features, embeddings) - loss = loss_fn(outputs, targets, others, cfg.data.mode) + # preload the next batch, if we're not on the last batch + if i < epoch_len - 1 and sampler is not None: + dataloader.preload(epoch_indices[i + 1]) - metrics = metrics_fn(outputs, targets, others, dist_manager, cfg.data.mode) + loss, metrics = forward_pass( + batch, model, precision, output_pad_size, dist_manager, cfg + ) if i == 0: total_metrics = metrics @@ -394,16 +464,17 @@ def val_epoch( } # Logging - total_loss += loss.item() + this_loss = loss.detach().item() + total_loss += this_loss end_time = time.time() duration = end_time - start_time start_time = end_time logger.info( - f"Val [{i}/{epoch_len}] Loss: {loss.item():.6f} Duration: {duration:.2f}s" + f"Val [{i}/{epoch_len}] Loss: {this_loss:.6f} Duration: {duration:.2f}s" ) - # We don't add individual loss measurements in the validation loop. + # We don't add individual loss measurements to tensorboard in the validation loop. avg_loss = total_loss / epoch_len avg_metrics = {k: v / epoch_len for k, v in total_metrics.items()} @@ -454,6 +525,25 @@ def main(cfg: DictConfig): logger.info(f"Config:\n{omegaconf.OmegaConf.to_yaml(cfg, resolve=True)}") + if cfg.training.precision == "float8": + # we have to manipulate the output shape + # to enable fp8 computations with transformer_engine. + # need the output to be divisible by 16. + # if (cfg.model.embedding_dim + cfg.model.functional_dim) % 16 != 0: + + if cfg.model.out_dim % 16 != 0: + # pad the output: + output_pad_size = 16 - (cfg.model.out_dim % 16) + cfg.model.out_dim += output_pad_size + logger.info( + f"Padding output dimension to {cfg.model.out_dim} for fp8 autocast" + ) + else: + output_pad_size = None + else: + input_pad_size = None + output_pad_size = None + # Set up model model = hydra.utils.instantiate(cfg.model) @@ -564,6 +654,7 @@ def main(cfg: DictConfig): train_dataset, train_sampler, model, + output_pad_size, optimizer, scheduler, logger, @@ -582,6 +673,7 @@ def main(cfg: DictConfig): val_dataset, val_sampler, model, + output_pad_size, logger, val_writer, epoch, From 2332a71d931a977c38adf2c8d1c4b2188f838df0 Mon Sep 17 00:00:00 2001 From: Corey Adams Date: Mon, 28 Jul 2025 20:19:33 +0000 Subject: [PATCH 14/23] Update model to not block multipl layers per mlp --- physicsnemo/models/transolver/transolver.py | 36 +++++++-------------- 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/physicsnemo/models/transolver/transolver.py b/physicsnemo/models/transolver/transolver.py index 51b953a2cb..129314411e 100644 --- a/physicsnemo/models/transolver/transolver.py +++ b/physicsnemo/models/transolver/transolver.py @@ -93,20 +93,20 @@ def __init__( self.linear_pre = linear_layer(n_input, n_hidden) self.linear_post = linear_layer(n_hidden, n_output) - # self.linears = nn.ModuleList( - # [ - # nn.Sequential(linear_layer(n_hidden, n_hidden), act()) - # for _ in range(n_layers) - # ] - # ) + self.linears = nn.ModuleList( + [ + nn.Sequential(linear_layer(n_hidden, n_hidden), act()) + for _ in range(n_layers) + ] + ) def forward(self, x): x = self.act(self.linear_pre(x)) - # for i in range(self.n_layers): - # if self.res: - # x = self.linears[i](x) + x - # else: - # x = self.linears[i](x) + for i in range(self.n_layers): + if self.res: + x = self.linears[i](x) + x + else: + x = self.linears[i](x) x = self.linear_post(x) return x @@ -180,18 +180,6 @@ def __init__( hidden_size=hidden_dim, ffn_hidden_size=hidden_dim * mlp_ratio, ) - # self.ln_mlp1 = nn.Sequential( - # te.LayerNorm(hidden_dim), - # MLP( - # hidden_dim, - # hidden_dim * mlp_ratio, - # hidden_dim, - # n_layers=0, - # res=False, - # act=act, - # use_te=True - # ), - # ) else: self.ln_mlp1 = nn.Sequential( nn.LayerNorm(hidden_dim), @@ -490,7 +478,7 @@ def get_grid(self, ref: int, batchsize: int = 1) -> torch.Tensor: def forward( self, - fx: torch.Tensor, + fx: torch.Tensor | None, embedding: torch.Tensor | None = None, T: torch.Tensor = None, ) -> torch.Tensor: From 93d33c26fb95f9f41f4dfcaf5e4ab00c6420be8f Mon Sep 17 00:00:00 2001 From: Corey Adams Date: Mon, 28 Jul 2025 20:29:28 +0000 Subject: [PATCH 15/23] Enabling domain parallelism. --- .../transolver/datapipe.py | 184 ++++++++----- .../transolver/metrics.py | 14 +- .../external_aerodynamics/transolver/train.py | 252 ++++++------------ 3 files changed, 202 insertions(+), 248 deletions(-) diff --git a/examples/cfd/external_aerodynamics/transolver/datapipe.py b/examples/cfd/external_aerodynamics/transolver/datapipe.py index 6d42b349f6..b1b65a1c3a 100644 --- a/examples/cfd/external_aerodynamics/transolver/datapipe.py +++ b/examples/cfd/external_aerodynamics/transolver/datapipe.py @@ -37,6 +37,10 @@ import numpy as np import zarr +import zarrs + +zarr.config.set({"codec_pipeline.path": "zarrs.ZarrsCodecPipeline"}) + import torch import torch.distributed as dist @@ -231,6 +235,56 @@ def __del__(self): if hasattr(self, "executor"): self.executor.shutdown(wait=True) + def _get_slice_boundaries( + self, zarr_array: zarr.Array + ) -> Tuple[int, int, tuple | None]: + # Determine what slice this rank should read + if self.device_mesh is not None: + # How many splits to make? + n_splits = dist.get_world_size(group=self.device_mesh.get_group()) + # What rank is this one? + this_rank = self.device_mesh.get_local_rank() + + sections = compute_split_shapes(zarr_array.shape[0], n_splits) + + global_chunk_start = sum(sections[:this_rank]) + global_chunk_stop = global_chunk_start + sections[this_rank] + + chunk_sizes = tuple( + (section,) + zarr_array.shape[1:] for section in sections + ) + + else: + global_chunk_start, global_chunk_stop = 0, zarr_array.shape[0] + chunk_sizes = None + + return global_chunk_start, global_chunk_stop, chunk_sizes + + def create_tensor_spec( + self, + zarr_array: zarr.Array, + key: str, + placements: tuple[Placement], + sharding_shapes: dict[int, tuple[int]] | None = None, + ) -> ShardTensorSpec: + + # Unpack the batch index: + shape = (1,) + zarr_array.shape + # Don't forget to unpack it in the sharding shapes too: + if sharding_shapes is not None: + for k in sharding_shapes.keys(): + sharding_shapes[k] = tuple((1,) + s for s in sharding_shapes[k]) + + stride = _stride_from_contiguous_shape_C_style(shape) + + meta = TensorMeta(shape, stride, to_torch_dtype(zarr_array.dtype)) + return ShardTensorSpec( + mesh=self.device_mesh, + placements=placements, + tensor_meta=meta, + _sharding_shapes=sharding_shapes, + ) + @profile def _read_key_chunk_aligned( self, zarr_group: zarr.Group, key: str, futures: list[Future] @@ -250,34 +304,14 @@ def _read_key_chunk_aligned( zarr_array = zarr_group[key] - # Determine what slice this rank should read - if self.device_mesh is not None: - # How many splits to make? - n_splits = dist.get_world_size(group=self.device_mesh.get_group()) - # What rank is this one? - this_rank = self.device_mesh.get_local_rank() - - sections = compute_split_shapes(zarr_array.shape[0], n_splits) - - global_chunk_start = sum(sections[:this_rank]) - global_chunk_stop = global_chunk_start + sections[this_rank] - - chunk_sizes = tuple( - (section,) + zarr_array.shape[1:] for section in sections - ) - stride = _stride_from_contiguous_shape_C_style(zarr_array.shape) - - meta = TensorMeta(zarr_array.shape, stride, zarr_array.dtype) - self.tensor_specs[key] = ShardTensorSpec( - mesh=self.device_mesh, - placements=(Shard(0),), - tensor_meta=meta, - _sharding_shapes={0: chunk_sizes}, + global_chunk_start, global_chunk_stop, chunk_sizes = self._get_slice_boundaries( + zarr_array + ) + if chunk_sizes is not None: + self.tensor_specs[key] = self.create_tensor_spec( + zarr_array, key, (Shard(1),), {0: chunk_sizes} ) - else: - global_chunk_start, global_chunk_stop = 0, zarr_array.shape[0] - # Calculate the shape of data this rank will read local_shape = [global_chunk_stop - global_chunk_start] + list( zarr_array.shape[1:] @@ -374,11 +408,10 @@ def _read_key_standard( if zarr_array.shape == (): if self.device_mesh is not None: - self.tensor_specs[key] = ShardTensorSpec( - mesh=self.device_mesh, - placements=(Replicate(),), - tensor_meta=TensorMeta(zarr_array.shape, (), zarr_array.dtype), - _sharding_shapes={}, + self.tensor_specs[key] = self.create_tensor_spec( + zarr_array, + key, + (Replicate(),), ) output = torch.from_numpy(np.array(zarr_array)) @@ -386,34 +419,44 @@ def _read_key_standard( output = output.pin_memory() return output + global_chunk_start, global_chunk_stop, chunk_sizes = self._get_slice_boundaries( + zarr_array + ) + if chunk_sizes is not None: + self.tensor_specs[key] = self.create_tensor_spec( + zarr_array, key, (Shard(1),), {0: chunk_sizes} + ) + + # Calculate the shape of data this rank will read + local_shape = [global_chunk_stop - global_chunk_start] + list( + zarr_array.shape[1:] + ) + # data = np.empty(zarr_array.shape, dtype=zarr_array.dtype) output = torch.empty( - zarr_array.shape, + local_shape, dtype=to_torch_dtype(zarr_array.dtype), pin_memory=self.pin_memory, ) data = output.numpy() slice = np.s_[:] + + # The zarr slice is not the numpy slice if we're sharding + if chunk_sizes is not None: + zarr_slice = np.s_[global_chunk_start:global_chunk_stop] + else: + zarr_slice = np.s_[:] + futures.append( self.executor.submit( _read_chunk_into_array, data, zarr_array, slice, + zarr_slice, ) ) - if self.device_mesh is not None: - # We need to track the tensor meta for this key - stride = _stride_from_contiguous_shape_C_style(zarr_array.shape) - - self.tensor_specs[key] = ShardTensorSpec( - mesh=self.device_mesh, - placements=(Replicate(),), - tensor_meta=TensorMeta(zarr_array.shape, stride, zarr_array.dtype), - _sharding_shapes={}, - ) - return output @profile @@ -426,28 +469,30 @@ def _read_zarr_file(self, filepath: Path) -> Dict[str, np.ndarray]: Returns: Dictionary mapping keys to numpy arrays or torch tensors. """ - with zarr.open_group(filepath, mode="r") as zarr_group: - data = {} - futures = [] + zarr_group = zarr.open_group(str(filepath), mode="r") + # group_keys = list(zarr_group.keys()) - # Process each key - for key in self.keys_to_read: + data = {} + futures = [] - if key not in zarr_group: - continue + # Process each key + for key in self.keys_to_read: - if key in self.large_keys: - # Use chunk-aligned reading for large data - data[key] = self._read_key_chunk_aligned(zarr_group, key, futures) - else: - # Use simple reading for other data - data[key] = self._read_key_standard(zarr_group, key, futures) + # if key not in group_keys: + # continue + + if key in self.large_keys: + # Use chunk-aligned reading for large data + data[key] = self._read_key_chunk_aligned(zarr_group, key, futures) + else: + # Use simple reading for other data + data[key] = self._read_key_standard(zarr_group, key, futures) - # Wait for all futures to complete - for future in futures: - future.result() + # Wait for all futures to complete + for future in futures: + future.result() - return data + return data @profile def _move_to_gpu(self, data: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]: @@ -461,16 +506,6 @@ def _move_to_gpu(self, data: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor] """ result = {} - # This is old code. - # Thinking about ways to reduce CPU overhead. - # If we could avoid the data reading step, it would be easier. - # total_size = 0 - # for key, array in data.items(): - # size = array.numel() * array.element_size() - # total_size += size - # print(f"Size in memory of {key}: {size / 1024 / 1024 / 1024} GB") - # print(f"Total size in memory: {total_size / 1024 / 1024 / 1024} GB") - with torch.cuda.stream(self.data_loader_stream): for key, array in data.items(): @@ -510,10 +545,13 @@ def _convert_to_shard_tensors( ) # Find out the desired placement: - if isinstance(self.placements, dict): - target_placement = self.placements[key] + if tensor.numel() > 1: + if isinstance(self.placements, dict): + target_placement = self.placements[key] + else: + target_placement = self.placements else: - target_placement = self.placements + target_placement = (Replicate(),) # Redistribute if necessary: # (Recall that this is one dimensional mesh only) diff --git a/examples/cfd/external_aerodynamics/transolver/metrics.py b/examples/cfd/external_aerodynamics/transolver/metrics.py index 18e8ecfa2e..5275710f06 100644 --- a/examples/cfd/external_aerodynamics/transolver/metrics.py +++ b/examples/cfd/external_aerodynamics/transolver/metrics.py @@ -16,7 +16,7 @@ import torch import torch.distributed as dist - +from physicsnemo.distributed import ShardTensor from physicsnemo.distributed import DistributedManager @@ -39,8 +39,16 @@ def all_reduce_dict( return metrics for key, value in metrics.items(): - dist.all_reduce(value) - value = value / dm.world_size + if isinstance(value, ShardTensor): + # Perform the reduction over the ddp axis, not the domain: + value = value.full_tensor() + mesh = dm.global_mesh["ddp"] + dist.all_reduce(value, group=mesh.get_group()) + value = value / mesh.size() + else: + dist.all_reduce(value) + value = value / dm.world_size + metrics[key] = value return metrics diff --git a/examples/cfd/external_aerodynamics/transolver/train.py b/examples/cfd/external_aerodynamics/transolver/train.py index 954dd42a2d..0a7702845a 100644 --- a/examples/cfd/external_aerodynamics/transolver/train.py +++ b/examples/cfd/external_aerodynamics/transolver/train.py @@ -23,6 +23,14 @@ from omegaconf import DictConfig from torch.utils.tensorboard import SummaryWriter + +from torch.distributed.fsdp import ( + FullyShardedDataParallel as FSDP, + ShardingStrategy, +) +from torch.distributed.tensor.placement_types import Shard +from torch.distributed.tensor import distribute_module + from physicsnemo.launch.utils import load_checkpoint, save_checkpoint from physicsnemo.launch.logging import PythonLogger, RankZeroLoggingWrapper from physicsnemo.distributed import DistributedManager @@ -33,6 +41,12 @@ from datapipe import DomainParallelZarrDataset from loss import loss_fn from metrics import metrics_fn +from preprocess import ( + preprocess_volume_data, + preprocess_surface_data, + downsample_volume, + downsample_surface, +) from contextlib import nullcontext from torch.amp import autocast, GradScaler @@ -87,165 +101,6 @@ def cast_precisions( return features, embeddings -@profile -def preprocess_surface_data( - batch: dict, -) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, dict]: - - """ - Preprocess the surface data. The functional input - is the air density and stream velocity. The embeddings - are the surface mesh centers and normals. The targets are - normalized to mean of 0, std 1. We cache the mean and std - to de-normalize when computing the metrics. - """ - - mesh_centers = batch["surface_mesh_centers"] - normals = batch["surface_normals"] - targets = batch["surface_fields"] - node_features = torch.stack( - [batch["air_density"], batch["stream_velocity"]], dim=-1 - ).to(torch.float32) - - # Normalize the surface fields: - norm_mean = targets.mean(dim=1) - norm_std = targets.std(dim=1) - targets = (targets - norm_mean) / norm_std - - # If you want to use this, be sure to updat the - # functional_dim value in your configuration - - # fourier_sin_features = [ - # torch.sin(mesh_centers * (2 ** i) * torch.pi) - # for i in range(4) - # ] - # fourier_cos_features = [ - # torch.cos(mesh_centers * (2 ** i) * torch.pi) - # for i in range(4) - # ] - - # Calculate center of mass - sizes = batch["stl_areas"] - centers = batch["stl_centers"] - - total_weighted_position = torch.einsum("ki,kij->kj", sizes, centers) - total_size = torch.sum(sizes) - center_of_mass = total_weighted_position[None, ...] / total_size - - # Subtract the COM from the centers: - mesh_centers = mesh_centers - center_of_mass - - embeddings = torch.cat( - [ - mesh_centers, - normals, - # *fourier_sin_features, - # *fourier_cos_features - ], - dim=-1, - ) - node_features = node_features.unsqueeze(1).broadcast_to(1, embeddings.shape[1], -1) - - others = { - "surface_areas": sizes, - "surface_normals": normals, - "stream_velocity": batch["stream_velocity"], - "air_density": batch["air_density"], - "norm_mean": norm_mean, - "norm_std": norm_std, - } - - return node_features, embeddings, targets, others - - -def preprocess_volume_data( - batch: dict, -) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, dict]: - """ - Preprocess the volumetric data. Right now, it's just - normalizing the targets and using the mesh centers as embeddings. - - The targets are normalized to mean of 0, std 1. We cache the - mean and std to de-normalize when computing the metrics. - """ - - mesh_centers = batch["volume_mesh_centers"] - targets = batch["volume_fields"] - node_features = torch.stack( - [batch["air_density"], batch["stream_velocity"]], dim=-1 - ).to(torch.float32) - - embeddings = mesh_centers - - # Normalize the surface fields: - norm_mean = targets.mean(dim=1) - norm_std = targets.std(dim=1) - targets = (targets - norm_mean) / norm_std - - node_features = node_features.unsqueeze(0).broadcast_to(1, embeddings.shape[1], -1) - - others = { - "norm_mean": norm_mean, - "norm_std": norm_std, - } - return node_features, embeddings, targets, others - - -@profile -def downsample_surface( - features: torch.Tensor, - embeddings: torch.Tensor, - targets: torch.Tensor, - num_keep=1024, -): - """ - Downsample the surface data. We generate one set of indices, and - use it to sample the same points from the features, embeddings, - and targets. Using torch.multinomial to sample without replacement. - """ - # Determine the number of samples to keep (e.g., 50% of original size) - num_samples = features.shape[1] - # Generate random indices to keep (faster for large num_samples) - indices = torch.multinomial( - torch.ones(num_samples, device=features.device), num_keep, replacement=False - ) - - # Use the same indices to downsample all tensors - downsampled_features = features[:, indices] - downsampled_embeddings = embeddings[:, indices] - downsampled_targets = targets[:, indices] - - return downsampled_features, downsampled_embeddings, downsampled_targets - - -@profile -def downsample_volume( - features: torch.Tensor, - embeddings: torch.Tensor, - targets: torch.Tensor, - num_keep=1024, -): - """ - Downsample the volume data. torch.multinomial has a limit of 2^24 - for num_samples, and the volumetric data typically exceeds that. - - So, this isjust sampling randomly with num_keep. The hope - is that the duplication is small ... but this needs to be refined. - """ - # Determine the number of samples to keep (e.g., 50% of original size) - num_samples = features.shape[1] - # The volume data is so large, that we'll sample randints - # which will very rarely duplicate - indices = torch.randint(0, num_samples, (num_keep,), device=features.device) - - # Use the same indices to downsample all tensors - downsampled_features = features[:, indices] - downsampled_embeddings = embeddings[:, indices] - downsampled_targets = targets[:, indices] - - return downsampled_features, downsampled_embeddings, downsampled_targets - - def forward_pass( batch: dict, model: torch.nn.Module, @@ -274,8 +129,17 @@ def forward_pass( # Cast precisions: features, embeddings = cast_precisions(features, embeddings, precision) - with get_autocast_context(precision): + print( + f"features shape: {features.shape} nd placements: {features._spec.placements}" + ) + print( + f"embeddings shape: {embeddings.shape} nd placements: {embeddings._spec.placements}" + ) + print( + f"targets shape: {targets.shape} nd placements: {targets._spec.placements}" + ) + print(f"cat shape: {torch.cat((features, embeddings), dim=-1).shape}") outputs = model(features, embeddings) if output_pad_size is not None: # Remove the padded outputs: @@ -332,6 +196,9 @@ def train_epoch( start_time = time.time() with Profiler(): for i, batch_idx in enumerate(epoch_indices): + + if i > 10: + break batch = dataloader[batch_idx] # preload the next batch, if we're not on the last batch if i < epoch_len - 1 and sampler is not None: @@ -445,6 +312,8 @@ def val_epoch( start_time = time.time() with torch.no_grad(): # Disable gradient computation for i, batch_idx in enumerate(epoch_indices): + if i > 10: + break # Get data from batch batch = dataloader[batch_idx] @@ -549,7 +418,39 @@ def main(cfg: DictConfig): model.to(dist_manager.device) - if dist_manager.world_size > 1: + # Configure domain parallelism, if enabled: + domain_size = int(cfg.training.domain_parallelism) + + if domain_size > 1: + # You can use -1 to one axis to indicate that you want to use all the GPUs in that dimension. + mesh = dist_manager.initialize_mesh( + mesh_shape=(-1, domain_size), mesh_dim_names=("ddp", "domain") + ) + # This is a subset of all the GPUs, and will vary depending on the process. + # Think of this as slicing the global mesh along the domain axis. + # It will contain only the GPUs that this process is sharing data with. + domain_mesh = mesh["domain"] + ddp_mesh = mesh["ddp"] + placements = (Shard(1),) + else: + domain_mesh = None + ddp_mesh = None + placements = None + + if domain_size > 1: + # Instead of DDP, for sharding we use FSDP. It's possible to use FSDP in the DDP + # mode, but since it's not pure data parallel we have to me more careful. + + # First, distribute the model so that each GPU has the copy with DTensor weights: + model = distribute_module(model, domain_mesh) + + model = FSDP( + model, + device_mesh=mesh["ddp"], + sharding_strategy=ShardingStrategy.NO_SHARD, + ) + + elif dist_manager.world_size > 1: model = torch.nn.parallel.DistributedDataParallel( model, device_ids=[dist_manager.local_rank], @@ -559,15 +460,11 @@ def main(cfg: DictConfig): num_params = sum(p.numel() for p in model.parameters()) logger.info(f"Number of parameters: {num_params}") - # Set up data - device_mesh = None - placements = None - # Training dataset train_dataset = DomainParallelZarrDataset( data_path=cfg.data.train.data_path, - device_mesh=device_mesh, + device_mesh=domain_mesh, placements=placements, max_workers=cfg.data.max_workers, pin_memory=cfg.data.pin_memory, @@ -579,7 +476,7 @@ def main(cfg: DictConfig): val_dataset = DomainParallelZarrDataset( data_path=cfg.data.val.data_path, # Assuming validation data path is configured - device_mesh=device_mesh, + device_mesh=domain_mesh, placements=placements, max_workers=cfg.data.max_workers, pin_memory=cfg.data.pin_memory, @@ -587,23 +484,34 @@ def main(cfg: DictConfig): large_keys=cfg.data.large_keys, ) + if ddp_mesh is not None: + num_replicas = ddp_mesh.size() + data_rank = ddp_mesh.get_local_rank() + else: + num_replicas = dist_manager.world_size + data_rank = dist_manager.rank + # Set up distributed samplers train_sampler = torch.utils.data.distributed.DistributedSampler( train_dataset, - num_replicas=dist_manager.world_size, - rank=dist_manager.rank, + num_replicas=num_replicas, + rank=data_rank, shuffle=True, drop_last=True, ) val_sampler = torch.utils.data.distributed.DistributedSampler( val_dataset, - num_replicas=dist_manager.world_size, - rank=dist_manager.rank, + num_replicas=num_replicas, + rank=data_rank, shuffle=False, # No shuffling for validation drop_last=True, ) + print( + f"num_replicas: {num_replicas}, data_rank: {data_rank}, val targets: {list(val_sampler)}" + ) + # Set up optimizer and scheduler optimizer = hydra.utils.instantiate(cfg.optimizer, params=model.parameters()) @@ -707,7 +615,7 @@ def launch(cfg: DictConfig): """ profiler = Profiler() # profiler.enable("torch") - # profiler.enable("line_profiler") + profiler.enable("line_profiler") profiler.initialize() main(cfg) profiler.finalize() From 0a1082c1d52dd2788149e7650b4556ae354fd176 Mon Sep 17 00:00:00 2001 From: Corey Adams Date: Mon, 28 Jul 2025 20:29:58 +0000 Subject: [PATCH 16/23] Isolate preprocess steps. --- .../transolver/preprocess.py | 282 ++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 examples/cfd/external_aerodynamics/transolver/preprocess.py diff --git a/examples/cfd/external_aerodynamics/transolver/preprocess.py b/examples/cfd/external_aerodynamics/transolver/preprocess.py new file mode 100644 index 0000000000..0121807e06 --- /dev/null +++ b/examples/cfd/external_aerodynamics/transolver/preprocess.py @@ -0,0 +1,282 @@ +# SPDX-FileCopyrightText: Copyright (c) 2023 - 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-FileCopyrightText: All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import torch + +from physicsnemo.distributed.shard_tensor import ShardTensor +from physicsnemo.utils.profiling import profile + + +@profile +def preprocess_surface_data( + batch: dict, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, dict]: + + """ + Preprocess the surface data. The functional input + is the air density and stream velocity. The embeddings + are the surface mesh centers and normals. The targets are + normalized to mean of 0, std 1. We cache the mean and std + to de-normalize when computing the metrics. + """ + + mesh_centers = batch["surface_mesh_centers"] + normals = batch["surface_normals"] + targets = batch["surface_fields"] + node_features = torch.stack( + [batch["air_density"], batch["stream_velocity"]], dim=-1 + ).to(torch.float32) + + # Normalize the surface fields: + norm_mean = targets.mean(dim=1) + norm_std = torch.sqrt(torch.mean((targets - norm_mean.unsqueeze(1)) ** 2, dim=1)) + targets = (targets - norm_mean) / norm_std + + # If you want to use this, be sure to updat the + # functional_dim value in your configuration + + # fourier_sin_features = [ + # torch.sin(mesh_centers * (2 ** i) * torch.pi) + # for i in range(4) + # ] + # fourier_cos_features = [ + # torch.cos(mesh_centers * (2 ** i) * torch.pi) + # for i in range(4) + # ] + + # Calculate center of mass + sizes = batch["stl_areas"] + centers = batch["stl_centers"] + + total_weighted_position = torch.einsum("ki,kij->kj", sizes, centers) + total_size = torch.sum(sizes) + center_of_mass = total_weighted_position[None, ...] / total_size + + # Subtract the COM from the centers: + mesh_centers = mesh_centers - center_of_mass + + embeddings = torch.cat( + [ + mesh_centers, + normals, + # *fourier_sin_features, + # *fourier_cos_features + ], + dim=-1, + ) + # node_features = node_features.unsqueeze(1).broadcast_to(1, embeddings.shape[1], -1) + + others = { + "surface_areas": sizes, + "surface_normals": normals, + "stream_velocity": batch["stream_velocity"], + "air_density": batch["air_density"], + "norm_mean": norm_mean, + "norm_std": norm_std, + } + print( + f"node_features shape: {node_features.shape} nd placements: {node_features._spec.placements}" + ) + print( + f"embeddings shape: {embeddings.shape} nd placements: {embeddings._spec.placements}" + ) + print(f"targets shape: {targets.shape} nd placements: {targets._spec.placements}") + return node_features, embeddings, targets, others + + +def preprocess_volume_data( + batch: dict, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, dict]: + """ + Preprocess the volumetric data. Right now, it's just + normalizing the targets and using the mesh centers as embeddings. + + The targets are normalized to mean of 0, std 1. We cache the + mean and std to de-normalize when computing the metrics. + """ + + mesh_centers = batch["volume_mesh_centers"] + sdf = batch["sdf_volume"] + targets = batch["volume_fields"] + node_features = torch.stack( + [batch["air_density"], batch["stream_velocity"]], dim=-1 + ).to(torch.float32) + + # SDF is missing a trailing dimension: + sdf = sdf.unsqueeze(-1) + + embeddings = torch.cat([mesh_centers, sdf], dim=-1) + + # Normalize the surface fields: + norm_mean = targets.mean(dim=1) + # norm_std = targets.std(dim=1) + norm_std = torch.sqrt(torch.mean((targets - norm_mean.unsqueeze(1)) ** 2, dim=1)) + + targets = (targets - norm_mean) / norm_std + + others = { + "norm_mean": norm_mean, + "norm_std": norm_std, + "stream_velocity": batch["stream_velocity"], + "air_density": batch["air_density"], + } + return node_features, embeddings, targets, others + + +@profile +def downsample_surface( + features: torch.Tensor, + embeddings: torch.Tensor, + targets: torch.Tensor, + num_keep=1024, +): + """ + Downsample the surface data. We generate one set of indices, and + use it to sample the same points from the features, embeddings, + and targets. Using torch.multinomial to sample without replacement. + """ + # Determine the number of samples to keep (e.g., 50% of original size) + + if isinstance(features, ShardTensor): + local_features = features.to_local() + local_embeddings = embeddings.to_local() + local_targets = targets.to_local() + + num_samples = local_embeddings.shape[1] + + indices = torch.multinomial( + torch.ones(num_samples, device=features.device), num_keep, replacement=False + ) + downsampled_embeddings = local_embeddings[:, indices] + downsampled_targets = local_targets[:, indices] + downsampled_features = features.unsqueeze(1).expand( + 1, downsampled_embeddings.shape[1], -1 + ) + + # Compute sharding shapes, replacing the local tensor num points with num_keep + output_embedding_shapes = {} + for k, shapes_tuple in embeddings._spec.sharding_shapes().items(): + updated_shape = tuple((s[0], num_keep, s[2:]) for s in shapes_tuple) + output_embedding_shapes[k] = updated_shape + + # Push the sampled versions back to shard tensors: + downsampled_embeddings = ShardTensor.from_local( + downsampled_embeddings, + device_mesh=embeddings._spec.mesh, + placements=embeddings._spec.placements, + sharding_shapes=output_embedding_shapes, + ) + downsampled_targets = ShardTensor.from_local( + downsampled_targets, + device_mesh=targets._spec.mesh, + placements=targets._spec.placements, + ) + downsampled_features = ShardTensor.from_local( + downsampled_features, + device_mesh=features._spec.mesh, + placements=embeddings._spec.placements, # NOTE! Changing the features placements here. + ) + + else: + + num_samples = embeddings.shape[1] + # Generate random indices to keep (faster for large num_samples) + indices = torch.multinomial( + torch.ones(num_samples, device=features.device), num_keep, replacement=False + ) + + # Use the same indices to downsample all tensors + downsampled_embeddings = embeddings[:, indices] + downsampled_targets = targets[:, indices] + downsampled_features = features.unsqueeze(1).expand( + 1, downsampled_embeddings.shape[1], -1 + ) + + return downsampled_features, downsampled_embeddings, downsampled_targets + + +@profile +def downsample_volume( + features: torch.Tensor, + embeddings: torch.Tensor, + targets: torch.Tensor, + num_keep=1024, +): + """ + Downsample the volume data. torch.multinomial has a limit of 2^24 + for num_samples, and the volumetric data typically exceeds that. + + So, this isjust sampling randomly with num_keep. The hope + is that the duplication is small ... but this needs to be refined. + """ + + if isinstance(features, ShardTensor): + # For shard tensors, we slice an approximately equal amount per device + + local_embeddings = embeddings.to_local() + local_targets = targets.to_local() + local_features = features.to_local() + + # Make a selection of num_keep per shard: + local_indices = torch.randint( + 0, local_embeddings.shape[1], (num_keep,), device=local_embeddings.device + ) + downsampled_embeddings = local_embeddings[:, local_indices] + downsampled_targets = local_targets[:, local_indices] + downsampled_features = local_features.unsqueeze(1).expand( + 1, downsampled_embeddings.shape[1], -1 + ) + + # Compute sharding shapes, replacing the local tensor num points with num_keep + output_embedding_shapes = {} + for k, shapes_tuple in embeddings._spec.sharding_shapes().items(): + updated_shape = tuple((s[0], num_keep, s[2:]) for s in shapes_tuple) + output_embedding_shapes[k] = updated_shape + + # Push the sampled versions back to shard tensors: + downsampled_embeddings = ShardTensor.from_local( + downsampled_embeddings, + device_mesh=embeddings._spec.mesh, + placements=embeddings._spec.placements, + sharding_shapes=output_embedding_shapes, + ) + downsampled_targets = ShardTensor.from_local( + downsampled_targets, + device_mesh=targets._spec.mesh, + placements=targets._spec.placements, + ) + downsampled_features = ShardTensor.from_local( + downsampled_features, + device_mesh=features._spec.mesh, + placements=embeddings._spec.placements, # NOTE! Changing the features placements here. + ) + + else: + # Determine the number of samples to keep (e.g., 50% of original size) + num_samples = features.shape[1] + # The volume data is so large, that we'll sample randints + # which will very rarely duplicate + indices = torch.randint(0, num_samples, (num_keep,), device=features.device) + + # Use the same indices to downsample all tensors + downsampled_embeddings = embeddings[:, indices] + downsampled_targets = targets[:, indices] + + downsampled_features = features.unsqueeze(1).expand( + 1, downsampled_embeddings.shape[1], -1 + ) + + return downsampled_features, downsampled_embeddings, downsampled_targets From 37d66e8e8ba07a858a70553d993c6b746380e29e Mon Sep 17 00:00:00 2001 From: Corey Adams Date: Wed, 30 Jul 2025 09:56:05 -0700 Subject: [PATCH 17/23] Fix bug in transolver physics attention base: the wrong normalization was used when projecting back onto output states. --- physicsnemo/models/transolver/Physics_Attention.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/physicsnemo/models/transolver/Physics_Attention.py b/physicsnemo/models/transolver/Physics_Attention.py index 556ea28ec3..861b1da308 100644 --- a/physicsnemo/models/transolver/Physics_Attention.py +++ b/physicsnemo/models/transolver/Physics_Attention.py @@ -138,15 +138,16 @@ def compute_slices_from_projections( # Cast to the computation type (since the parameter is probably fp32) slice_weights = slice_weights.to(slice_projections.dtype) - # Average the slices over the token dimension - slice_norm = slice_weights.sum(2) # [Batch, N_heads, Slice_num] + # This does the projection of the latent space fx by the weights: # Computing the slice tokens is a matmul followed by a normalization. # It can, unfortunately, overflow in reduced precision, so normalize first: - slice_weights = slice_weights / (slice_norm[:, :, None, :] + 1e-2) - slice_token = torch.matmul(slice_weights.transpose(2, 3), fx) + slice_norm = slice_weights.sum(2) # [Batch, N_heads, Slice_num] + normed_weights = slice_weights / (slice_norm[:, :, None, :] + 1e-2) + slice_token = torch.matmul(normed_weights.transpose(2, 3), fx) + # Return the original weights, not the normed weights: return slice_weights, slice_token def compute_slice_attention(self, slice_tokens: torch.Tensor) -> torch.Tensor: @@ -261,6 +262,8 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: # Deslice: outputs = self.project_attention_outputs(out_slice_token, slice_weights) + # print(f"outputs mean and shape: {outputs.mean()} and {outputs.shape}") + # Outputs now has the same shape as the original input x return outputs From 8b61a37302d500fa72b3f0df07d1e34c59d97e90 Mon Sep 17 00:00:00 2001 From: Corey Adams Date: Thu, 31 Jul 2025 11:24:09 -0700 Subject: [PATCH 18/23] Add script for computing normalization factors for transolver. --- .../transolver/compute_normalizations.py | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 examples/cfd/external_aerodynamics/transolver/compute_normalizations.py diff --git a/examples/cfd/external_aerodynamics/transolver/compute_normalizations.py b/examples/cfd/external_aerodynamics/transolver/compute_normalizations.py new file mode 100644 index 0000000000..b01cd6a618 --- /dev/null +++ b/examples/cfd/external_aerodynamics/transolver/compute_normalizations.py @@ -0,0 +1,132 @@ +# SPDX-FileCopyrightText: Copyright (c) 2023 - 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-FileCopyrightText: All rights reserved. +# SPDX-License-Identifier: MIT License +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import os +import numpy as np +import torch +from omegaconf import OmegaConf + +from datapipe import DomainParallelZarrDataset + +""" +This file provides utilities to compute normalization statistics (mean, std, min, max) +for a given field in a dataset, typically used for preprocessing in CFD workflows. +""" + + +def compute_mean_std_min_max( + dataset: DomainParallelZarrDataset, field_key: str +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: + """ + Compute the mean, standard deviation, minimum, and maximum for a specified field + across all samples in a dataset. + + Uses a numerically stable online algorithm for mean and variance. + + Args: + dataset (DomainParallelZarrDataset): The dataset to process. + field_key (str): The key for the field to normalize. + + Returns: + tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: + mean, std, min, max tensors for the field. + """ + N = 0 # Total number of elements processed + mean = None + M2 = None # Sum of squares of differences from the current mean + min_val = None + max_val = None + + for i in range(len(dataset)): + print(f"reading file: {i}") + data = dataset[i][field_key] + if mean is None: + # Initialize accumulators based on the shape of the data + mean = torch.zeros(data.shape[-1], device=data.device) + M2 = torch.zeros(data.shape[-1], device=data.device) + min_val = torch.full((data.shape[-1],), float("inf"), device=data.device) + max_val = torch.full((data.shape[-1],), float("-inf"), device=data.device) + n = data.shape[1] + N += n + + # Compute batch statistics + batch_mean = data.mean(axis=(0, 1)) + batch_M2 = ((data - batch_mean) ** 2).sum(axis=(0, 1)) + batch_n = data.shape[1] + + # Update min/max + batch_min = data.amin(dim=(0, 1)) + batch_max = data.amax(dim=(0, 1)) + min_val = torch.minimum(min_val, batch_min) + max_val = torch.maximum(max_val, batch_max) + + # Update running mean and M2 (Welford's algorithm) + delta = batch_mean - mean + N += batch_n + mean = mean + delta * (batch_n / N) + M2 = M2 + batch_M2 + delta**2 * (batch_n * N) / N + + var = M2 / (N - 1) + std = torch.sqrt(var) + return mean, std, min_val, max_val + + +if __name__ == "__main__": + """ + Script entry point for computing normalization statistics for a specified field + in a dataset, using configuration from a YAML file. + + The computed statistics are printed and saved to a .npz file. + """ + # Edit this path to your config file or hardcode the config as needed + config_path: str = "conf/train_surface.yaml" + cfg = OmegaConf.load(config_path) + + # Choose which field to normalize + field_key: str = "surface_fields" + + # Create the dataset using configuration parameters + dataset = DomainParallelZarrDataset( + data_path=cfg.data.train.data_path, + device_mesh=None, + placements=None, + max_workers=cfg.data.max_workers, + pin_memory=cfg.data.pin_memory, + keys_to_read=[field_key], + large_keys=[field_key], + ) + + # Compute normalization statistics + mean, std, min_val, max_val = compute_mean_std_min_max(dataset, field_key) + print(f"Mean for {field_key}: {mean}") + print(f"Std for {field_key}: {std}") + print(f"Min for {field_key}: {min_val}") + print(f"Max for {field_key}: {max_val}") + + # Save statistics to a .npz file for later use + np.savez( + f"{field_key}_normalization.npz", + mean=mean.cpu().numpy(), + std=std.cpu().numpy(), + min=min_val.cpu().numpy(), + max=max_val.cpu().numpy(), + ) From 0253bf6a7b9314d1b19735abfbd20ee9ee4e588c Mon Sep 17 00:00:00 2001 From: Corey Adams Date: Thu, 31 Jul 2025 11:25:10 -0700 Subject: [PATCH 19/23] remove volume config for now. --- .../transolver/conf/train_volume.yaml | 92 ------------------- 1 file changed, 92 deletions(-) delete mode 100644 examples/cfd/external_aerodynamics/transolver/conf/train_volume.yaml diff --git a/examples/cfd/external_aerodynamics/transolver/conf/train_volume.yaml b/examples/cfd/external_aerodynamics/transolver/conf/train_volume.yaml deleted file mode 100644 index 97f24b9134..0000000000 --- a/examples/cfd/external_aerodynamics/transolver/conf/train_volume.yaml +++ /dev/null @@ -1,92 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023 - 2024 NVIDIA CORPORATION & AFFILIATES. -# SPDX-FileCopyrightText: All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - - -output_dir: "runs" -run_id: "volume/OneCycleLR/noTE/bfloat16/" - -# Training configuration -training: - precision: bfloat16 # float32, float16, bfloat16 - num_epochs: 250 - save_interval: 25 # Save checkpoint every N epochs - seed: 42 - output_dir: "runs" - compile: true - -# Model configuration -model: - _target_: physicsnemo.models.transolver.Transolver - functional_dim: 2 # Input feature dimension - out_dim: 5 # Output feature dimension - embedding_dim: 3 # Spatial embedding dimension - n_layers: 8 # Number of transformer layers - n_hidden: 256 # Hidden dimension - dropout: 0.0 # Dropout rate - n_head: 8 # Number of attention heads - act: "gelu" # Activation function - mlp_ratio: 4 # MLP ratio in attention blocks - slice_num: 128 # Number of slices in physics attention - unified_pos: false # Whether to use unified positional embeddings - ref: 8 # Reference dimension for unified pos - structured_shape: null - use_te: false # Use transformer engine - Time_Input: false # Whether to use time embeddings - -scheduler: - name: "OneCycleLR" - params: - final_div_factor: 1e4 - -# scheduler: -# name: "ReduceLROnPlateau" -# params: -# min_lr: 1e-6 - - -# Optimizer configuration -optimizer: - _target_: torch.optim.AdamW - lr: 1.0e-3 - weight_decay: 1.0e-4 - betas: [0.9, 0.999] - eps: 1.0e-8 - -# Data configuration -data: - train: - data_path: /user_data/datasets/domino/train/ - val: - data_path: /user_data/datasets/domino/val/ - max_workers: 32 - pin_memory: true - resolution: 200_000 - mode: volume - data_keys: - - "volume_mesh_centers" - - "volume_fields" - - "air_density" - - "stream_velocity" - large_keys: - - "volume_fields" - - "volume_mesh_centers" - -# Logging configuration -logging: - level: INFO - format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s' - From 0313c338429c649b2836f234d744705e352b01b5 Mon Sep 17 00:00:00 2001 From: Corey Adams Date: Thu, 31 Jul 2025 12:24:27 -0700 Subject: [PATCH 20/23] Bulk update of transolver model and example for CFD external aero. --- .../transolver/README.md | 208 +++++++----- .../transolver/conf/train_surface.yaml | 37 +-- .../transolver/datapipe.py | 1 - .../transolver/inference_on_stl.py | 305 ++++++++++++++++++ .../transolver/inference_on_zarr.py | 169 ++++++++++ .../external_aerodynamics/transolver/loss.py | 2 +- .../transolver/metrics.py | 15 +- .../transolver/preprocess.py | 31 +- .../transolver/requirements.txt | 4 +- .../external_aerodynamics/transolver/train.py | 159 ++++----- physicsnemo/models/transolver/transolver.py | 2 +- 11 files changed, 728 insertions(+), 205 deletions(-) create mode 100644 examples/cfd/external_aerodynamics/transolver/inference_on_stl.py create mode 100644 examples/cfd/external_aerodynamics/transolver/inference_on_zarr.py diff --git a/examples/cfd/external_aerodynamics/transolver/README.md b/examples/cfd/external_aerodynamics/transolver/README.md index 80b6a00474..40dfef7a0d 100644 --- a/examples/cfd/external_aerodynamics/transolver/README.md +++ b/examples/cfd/external_aerodynamics/transolver/README.md @@ -1,4 +1,6 @@ -# Transolver CFD Example: Code Overview +# Transolver for External Aerodynamics on Irregular Meshes + +## Transolver CFD Example: Overview This directory contains the essential components for training and evaluating a Transolver model tailored to external aerodynamics CFD problems. The Transolver model @@ -18,31 +20,115 @@ TransformerEngine. The training workflow for Transolver leverages the same input datasets as DoMINO. For more information on the datasets, refer to the external_aerodynamics example for DoMINO and the [PhysicsNeMo Curator](https://github.com/NVIDIA/physicsnemo-curator) for data -preparation guidance. Unlike DoMINO, Transolver does not require neighbor points or any -explicit structure (such as graph connections or edges) in the input data, resulting in -a much simpler datapipe that is fully encapsulated within these examples. - -Below, we provide a high-level overview of the main files and their roles in the -workflow. - ---- - -## 1. `src/train.py` - -This script serves as the main entry point for training the DoMINO/Transolver model, -utilizing distributed data parallelism. The training loop processes the entire dataset, -computing a simple point-wise relative L2 loss for either surface or volume data, and -includes downsampling to handle the high native mesh resolutions. - -The script is designed for multi-GPU training using PyTorch’s DistributedDataParallel, -and it loads datasets through a custom datapipe that supports distributed sampling. -Model instantiation is flexible, supporting either surface or volume predictions -as specified in the configuration. - -The script supports mixed-precision training with gradient -scaling, and it manages checkpointing with the physicsnemo checkpointing utils. -Throughout training, metrics are logged to both TensorBoard and the console and -validation is performed after each epoch. +preparation guidance. + +> **Note:** Currently, training transolver in this example supports **surface** data only. +> Volumetric data is still in development. + +## Requirements + +Transolver requires TransformerEngine from NVIDIA, as well as Zarr >= 3.0 and `zarrs` +for the data pipeline. Install them with `pip install -r requirements.txt` + +> For the Transolver datapipe, zarr > 3.0 is required. If you are using an older +> container, you may need to `unset PIP_CONSTRAINTS` to allow zarr 3.0 or higher. + +## Using Transolver for External Aerodynamics + +1. Prepare the Dataset. Follow the guidance from DoMINO and PhysicsNemo-Curator +to prepare your dataset into a Zarr format. We also use the dataset to compute +normalization factors for the dataset, which help stabilize training. + +2. Train your model. The model and training configuration is set in +`conf/train_surface.yaml`, where you can control both network properties +and training properties. See below for an overview and explanation of key +parameters that may be of special interest. + +3. Use the trained model to perform inference. This example contains two +inference examples: one for inference on the validation set, already in +Zarr format, and a second example for inference directly on .vtp files. + +The following sections contain further details on the training and inference +recipe. + +## Model Training + +To train the model, first we compute normalization factors on the dataset to +make the predictive quantities output in a well defined range. The includec +script, `compute_normalizations.py`, will compute either the normalization +factors. Once run, it should save to an output file similar to +"surface_fields_normalization.npz". This will get loaded during training. +Check the training script to ensure the right path is used for your normalization +factors - it's not a configuration parameter but directly encoded in the script. + +> By default, the normalization sets the mean to 0.0 and std to 1.0 of all labels +> in the dataset, computing the mean across the train dataset. You could adapt +> this to a different normalization, however take care to update both the +> preprocessing as well as inference scripts. Min/Max is another popular strategy. + +To configure your training run, use `hydra` and `conf/train_surface.yaml`. The +config contains sections for the model, data, optimizer, and training settings. +For details on the model parameters, see the API for `physicsnemo.models.transolver`. +The data is processed with a custom Zarr dataloader, designed to use zarr 3.0 and +`zarrs` rust implementation for an optimized Codec. It also uses python's `threading` +module to open parallel reads of multiple zarr keys. You can control the number +of parallel python threads via `data.max_workers`. + +Additionally, the Zarr dataloader optimizes CPU->GPU transfers by directly +allocating pinned memory on the CPU, reading the Zarr data into that +memory buffer via a 0-copy to numpy, and moving the data to GPU via a separate +stream with non-blocking transfers. In short: you can completely overlap IO +and GPU processing as long as the IO file system can provide the next data example +fast enough. In reality, the IO latency has some variance but is not a bottleneck. + +You can disable memory pinning with `data.pin_memory=False`. Further, to fit +the training into memory, you can apply on-the-fly downsampling to the data +with `data.resolution=N`, where `N` is how many points per GPU to use. This dataloader +will yield the full data examples in shapes of `[1, K, f]` where `K` is the resolution +of the mesh, and `f` is the feature space (3 for points, normals, etc. 4 for surface +fields). Downsampling happens in the preprocessing pipeline. + +> The pipeline has the ability to optimally load data from disk into `physicsnemo.ShardTensor` +> for domain parallelism - however the model support is still in development. + +During training, the configuration uses the OneCyle learning rate (similar to the +original Transolver publication), and float32 format. The scheduler and learning rate +may be configured - note that the scheduler is updated every training step. For +schedulers that update every epoch, modification of the training script may be required. + +### Training Precision + +Transolver, as a transformer-like architecture, has support for NVIDIA's +[TransformerEngine](https://docs.nvidia.com/deeplearning/transformer-engine/user-guide/index.html) +built in. You can enable/disable the transformer engine path in the model with +`model.use_te=[True | False]`. Available precisions for training with `transformer_engine` +are `training.precision=["float32" | "float16" | "bfloat16" | "float8" ]`. In `float8` +precision, the TransformerEngine Hybrid recipe is used for casting weights and inputs +in the forward and backwards passes. For more details on `float8` precision, see +the fp8 guide from +[TransformerEngine](https://docs.nvidia.com/deeplearning/transformer-engine/user-guide/examples/fp8_primer.html). +When using fp8, the training script will automatically pad and unpad the input and output, +respectively, to use the fp8 hardware correctly. + +> **Float8** precisions are only available on GPUs with fp8 tensorcore support, such +> as Hopper, Blackwell, Ada Lovelace, and others. + +### Other Configuration Settings + +Several other important configuration settings are available: + +- `training.compile` will use `torch.compile` for optimized performance. It is not +compatible with `transformer_engine` (`model.use_te=True`). If TransformerEngine is +not used, and half precision is, `torch.compile` is recommended for improved performance. +- `training.num_epochs` controls the total number of epochs used during training. +- `training.save_interval` will dictate how often the model weights and training +tools are checkpointed. + +> **Note** Like other parameters of the model, changing the value of `model.use_te` +> will make checkpoints incompatible. + +The training script supports data-parallel training via PyTorch DDP. In a future +update, we may enable domain parallelism via FSDP and ShardTensor. The script can be launched on a single GPU with @@ -97,58 +183,28 @@ Epoch 47 Validation Average Metrics: +-------------+---------------------+ ``` ---- - -## 2. `conf/train_volume.yaml` and `conf/train_surface.yaml` - -These configuration files define all the settings required for a training run. They -specify output directories, run identifiers, random seeds, and precision settings, as -well as the number of epochs, checkpoint intervals, and whether to use compilation. The -model architecture is described here, including input and output dimensions, the number -of layers, embedding size, attention heads, and activation functions. Optimizer -settings such as the type (AdamW), learning rate, weight decay, and other -hyperparameters are also included. Data-related settings cover paths to the training and -validation datasets, worker and thread counts, memory pinning, and which data keys to -load. Finally, logging preferences are set, controlling the level and format of output. - -Note that, to use TransformerEngine, you must enable it directly in the model settings. -TransformerEngine is incompatible with `torch.compile`. - ---- - -## 3. `loss.py` - -This file defines the loss functions used during Transolver training, primarily -focusing on a relative L2 loss. For surface data, it computes mean squared error (MSE) -or root mean squared error (RMSE) for both pressure (a scalar) and wall shear (a -vector), handling each component separately before combining the results. - ---- - -## 4. `metrics.py` +## Dataset Inference -Evaluation metrics for model predictions are defined here. The metrics are designed to -aggregate results across distributed processes, ensuring consistency in multi-GPU -setups. For surface predictions, the script computes normalized L2 errors for both -pressure and shear components, after unnormalizing the predictions and targets. The -structure of the code allows for straightforward extension to additional metrics or -application domains as needed. +There are two scripts provided as inference examples - it's expected that every user's +inference workloads are different, so these aim to cover common scenarios as examples. ---- +First, the validation dataset in Zarr format can be loaded, processed, and the L2 +metrics summarized in `inference_on_zarr.py`. Alternatively, the model can be used +directly on `.vtp` or `.stl` files as shown in `inference_on_vtp.py`. Note that the +script contains several parameters from the DrivaerML dataset as hardcoded variable +names: `CpMeanTrim`, `pMeanTrim`, `wallShearStressMeanTrim`, which are used to +compute the L2 metrics on the inference outputs. -## 5. `datapipe.py` +In `inference_on_zarr.py`, the dataset examples are downsampled and preprocessed +exactly as in the training script. In `inference_on_stl.py`, however, the entire +mesh is processed. To enable the mesh to fit into GPU memory, the mesh is chunked +into pieces that are then processed, and recombined to form the prediction on the +entire mesh. The outputs are then saved to .vtp files for downstream analysis. -Efficient loading of large CFD datasets stored in Zarr format is handled by this file, -which implements like a PyTorch dataset. The datapipe reads large arrays in a chunk-aligned -manner for optimal performance, leveraging threads for parallel I/O. It offers -flexibility in specifying which data keys to load and which are considered “large” (and -thus read in chunks, vs. small arrays which are directly read in their entirety). -For faster GPU transfers, it can allocate directly to pinned memory, then share buffers -with numpy for streaming from disk directly to pinned memory. The dataload has a -prefetch utility, as well, enabling the dataloader to queue the next batch to GPU. +## Future work -CPU to GPU transfers are performed in a separate CUDA stream from the main computation, -enabling async transfers overlapping with model training. The main parameter to tune -is the number of workers for the threading: too many, and you will introduce -CPU overhead limiting the model performance. Too few, and the dataload won't acheive -peak throughput of dataloading. +The Transolver model is a promising, transformer-based model that produces high +quality predictions for CFD surrogate simulations. In the future, we may update +the example to include domain parallelism and Transolver++ enhancements, +as well as volumetric data examples. If you +have issues, requests, or other items please feel free to open an issue and discuss! diff --git a/examples/cfd/external_aerodynamics/transolver/conf/train_surface.yaml b/examples/cfd/external_aerodynamics/transolver/conf/train_surface.yaml index 76b9c6e691..67d751fc50 100644 --- a/examples/cfd/external_aerodynamics/transolver/conf/train_surface.yaml +++ b/examples/cfd/external_aerodynamics/transolver/conf/train_surface.yaml @@ -17,16 +17,14 @@ output_dir: "runs" -run_id: "surface/ReduceLR/TE/float16-3/" +run_id: "surface/precision-convergence/float32" # Training configuration training: - precision: float16 # float32, float16, bfloat16 - num_epochs: 500 + precision: float32 # float32, float16, bfloat16 + num_epochs: 251 # Add one to save at 250 :) save_interval: 25 # Save checkpoint every N epochs - seed: 42 - output_dir: "runs" - compile: false + compile: True # Model configuration model: @@ -44,26 +42,26 @@ model: unified_pos: false # Whether to use unified positional embeddings ref: 8 # Reference dimension for unified pos structured_shape: null - use_te: True # Use transformer engine + use_te: false # Use transformer engine Time_Input: false # Whether to use time embeddings -# scheduler: -# name: "OneCycleLR" -# params: -# final_div_factor: 1e4 - -# StepLR scheduler: Decays the learning rate by gamma every step_size epochs scheduler: - name: "StepLR" + name: "OneCycleLR" params: - step_size: 50 # Decay every 100 epochs (set X as desired) - gamma: 0.5 # Decay factor + final_div_factor: 1e4 + +# # StepLR scheduler: Decays the learning rate by gamma every step_size epochs +# scheduler: +# name: "StepLR" +# params: +# step_size: 50 # Decay every 100 epochs (set X as desired) +# gamma: 0.5 # Decay factor # Optimizer configuration optimizer: _target_: torch.optim.AdamW - lr: 3.0e-4 + lr: 5.0e-4 weight_decay: 1.0e-4 betas: [0.9, 0.999] eps: 1.0e-8 @@ -74,10 +72,9 @@ data: data_path: /user_data/datasets/domino/train/ val: data_path: /user_data/datasets/domino/val/ - max_workers: 64 + max_workers: 8 pin_memory: true - # resolution: 10000 - resolution: 325_000 + resolution: 400_000 mode: surface data_keys: - "surface_fields" diff --git a/examples/cfd/external_aerodynamics/transolver/datapipe.py b/examples/cfd/external_aerodynamics/transolver/datapipe.py index b1b65a1c3a..36772f961a 100644 --- a/examples/cfd/external_aerodynamics/transolver/datapipe.py +++ b/examples/cfd/external_aerodynamics/transolver/datapipe.py @@ -660,7 +660,6 @@ def __getitem__(self, idx: int) -> Dict[str, torch.Tensor | ShardTensor]: else: filename = self.filenames[idx] filepath = self.data_path / filename - # Read data from zarr file data = self._read_zarr_file(filepath) data = self._move_to_gpu(data) diff --git a/examples/cfd/external_aerodynamics/transolver/inference_on_stl.py b/examples/cfd/external_aerodynamics/transolver/inference_on_stl.py new file mode 100644 index 0000000000..b776a9ef22 --- /dev/null +++ b/examples/cfd/external_aerodynamics/transolver/inference_on_stl.py @@ -0,0 +1,305 @@ +# SPDX-FileCopyrightText: Copyright (c) 2023 - 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-FileCopyrightText: All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import torch + +import hydra +from omegaconf import DictConfig + +import pyvista as pv +from physicsnemo.models.transolver.transolver import Transolver +from physicsnemo.launch.utils import load_checkpoint + +from physicsnemo.distributed import DistributedManager + +import vtk +from vtk.util import numpy_support +import os +import math +import time + + +def read_data_from_stl(stl_path, air_density=1.2050, stream_velocity=30.0): + + dm = DistributedManager() + + mesh = pv.read(stl_path) + + batch = {} + + batch["surface_mesh_centers"] = np.asarray(mesh.cell_centers().points) + + normals = np.asarray(mesh.cell_normals) + # Normalize cell normals + surface_normals = ( + surface_normals / np.linalg.norm(surface_normals, axis=1)[:, np.newaxis] + ) + batch["surface_normals"] = surface_normals + surface_areas = mesh.compute_cell_sizes(length=False, area=True, volume=False) + batch["surface_mesh_sizes"] = np.array(surface_areas.cell_data["Area"]) + + batch["air_density"] = np.array([air_density], dtype="float32") + batch["stream_velocity"] = np.array([stream_velocity], dtype="float32") + + batch = { + k: torch.from_numpy(v).to(device=dm.device, dtype=torch.float32) + for k, v in batch.items() + } + + batch = {k: torch.unsqueeze(v, dim=0) for k, v in batch.items()} + + return batch + + +def read_data_from_vtp(vtp_path, air_density=1.2050, stream_velocity=30.0): + + dm = DistributedManager() + + mesh = pv.read(vtp_path) + + batch = {} + + batch["surface_mesh_centers"] = np.asarray(mesh.cell_centers().points) + batch["surface_normals"] = np.asarray(mesh.cell_normals) + surface_areas = mesh.compute_cell_sizes(length=False, area=True, volume=False) + batch["surface_mesh_sizes"] = np.array(surface_areas.cell_data["Area"]) + + batch["CpMeanTrim"] = np.asarray(mesh.cell_data["CpMeanTrim"]) + batch["pMeanTrim"] = np.asarray(mesh.cell_data["pMeanTrim"]) + batch["wallShearStressMeanTrim"] = np.asarray( + mesh.cell_data["wallShearStressMeanTrim"] + ) + + batch["air_density"] = np.array([air_density], dtype="float32") + batch["stream_velocity"] = np.array([stream_velocity], dtype="float32") + + # From VTP we can also exctract the ground-truth results: + batch = { + k: torch.from_numpy(v).to(device=dm.device, dtype=torch.float32) + for k, v in batch.items() + } + + batch = {k: torch.unsqueeze(v, dim=0) for k, v in batch.items()} + + return mesh, batch + + +def preprocess_data( + batch, +): + + mesh_centers = batch["surface_mesh_centers"] + normals = batch["surface_normals"] + node_features = torch.stack( + [batch["air_density"], batch["stream_velocity"]], axis=-1 + ) + + # Calculate center of mass + sizes = batch["surface_mesh_sizes"] + + total_weighted_position = torch.einsum("ki,kij->kj", sizes, mesh_centers) + total_size = torch.sum(sizes) + center_of_mass = total_weighted_position[None, ...] / total_size + + # Subtract the COM from the centers: + mesh_centers = mesh_centers - center_of_mass + + embeddings = torch.cat( + [ + mesh_centers, + normals, + ], + dim=-1, + ) + node_features = node_features.expand(1, embeddings.shape[1], -1) + + return node_features, embeddings + + +def process_vtp_file( + vtp_file, model, norm_factors, dist_manager, output_folder, batch_size=500_000 +): + + # First, load the data and mesh from the file: + try: + mesh, batch = read_data_from_vtp(vtp_file) + except FileNotFoundError as e: + print(f"File not found: {vtp_file}") + return + + # Run preprocessing to prepare the data for the model: + fx, embedding = preprocess_data(batch) + + with torch.no_grad(): + + if batch_size > fx.shape[1]: + + prediction = ( + model(fx, embedding) * norm_factors["std"] + norm_factors["mean"] + ) + + else: + # Split the indices by a batch size. We shuffle the cells into + # the batches (don't forget to unshuffle later!) + indices = torch.randperm(fx.shape[1], device=fx.device) + + index_blocks = torch.split(indices, batch_size) + + predictions = [] + for i, index_block in enumerate(index_blocks): + local_fx = fx[:, index_block] + local_embedding = embedding[:, index_block] + predictions.append( + model(local_fx, local_embedding) * norm_factors["std"] + + norm_factors["mean"] + ) + + prediction = torch.cat(predictions, dim=1) + + # Now, we have to *unshuffle* the prediction to the original index + inverse_indices = torch.empty_like(indices) + inverse_indices[indices] = torch.arange( + indices.size(0), device=indices.device + ) + # Suppose prediction is of shape [batch, N, ...] + prediction = prediction[:, inverse_indices] + + pred_pressure, pred_shear = torch.split(prediction, (1, 3), dim=-1) + pred_pressure = pred_pressure.squeeze(-1) + + pred_pressure = pred_pressure * ( + batch["air_density"] * batch["stream_velocity"] ** 2 + ) + pred_shear = pred_shear * (batch["air_density"] * batch["stream_velocity"] ** 2) + target_pressure = batch["pMeanTrim"] + target_shear = batch["wallShearStressMeanTrim"] + + pressure_l2_num = (pred_pressure - target_pressure) ** 2 + pressure_l2_num = torch.sum(pressure_l2_num, dim=1) + pressure_l2_num = torch.sqrt(pressure_l2_num) + + pressure_l2_denom = target_pressure**2 + pressure_l2_denom = torch.sum(pressure_l2_denom, dim=1) + pressure_l2_denom = torch.sqrt(pressure_l2_denom) + + pressure_l2 = pressure_l2_num / pressure_l2_denom + + shear_l2_num = (pred_shear - target_shear) ** 2 + shear_l2_num = torch.sum(shear_l2_num, dim=1) + shear_l2_num = torch.sqrt(shear_l2_num) + + shear_l2_denom = target_shear**2 + shear_l2_denom = torch.sum(shear_l2_denom, dim=1) + shear_l2_denom = torch.sqrt(shear_l2_denom) + + shear_l2 = shear_l2_num / shear_l2_denom + + print(f"pressure l2: {pressure_l2}") + print(f"shear l2: {shear_l2}") + + # Write the output to a new .vtp file. Clone the old information: + output_mesh = mesh.copy() + # Convert tensors to numpy arrays and squeeze batch dimension + pred_pressure_np = pred_pressure[0].cpu().numpy() + pred_shear_np = pred_shear[0].cpu().numpy() + # Add arrays to the mesh as cell data + output_mesh.cell_data["PredictedPressure"] = pred_pressure_np + output_mesh.cell_data["PredictedShear"] = pred_shear_np + # Ensure the output directory exists + os.makedirs(output_folder, exist_ok=True) + # Construct output file path + base_name = os.path.basename(vtp_file) + output_path = os.path.join(output_folder, f"pred_{base_name}") + # Write to file + output_mesh.save(output_path) + print(f"Saved prediction VTP to: {output_path}") + + +def inference_on_vtp_or_stl(cfg: DictConfig): + + DistributedManager.initialize() + + dist_manager = DistributedManager() + + run_id = cfg.run_id + + # Set up model + model = hydra.utils.instantiate(cfg.model) + model.eval() + model.to(dist_manager.device) + + ckpt_args = { + "path": f"{cfg.output_dir}/{cfg.run_id}/checkpoints", + "models": model, + } + + # Load the normalization factors: + norm_file = "surface_fields_normalization.npz" + norm_data = np.load(norm_file) + norm_factors = { + "mean": torch.from_numpy(norm_data["mean"]).to(dist_manager.device), + "std": torch.from_numpy(norm_data["std"]).to(dist_manager.device), + } + + # Restore the model: + loaded_epoch = load_checkpoint(device=dist_manager.device, **ckpt_args) + print(f"loaded epoch: {loaded_epoch}") + + stl_input_path = "/group_data/datasets/drivaer_aws/drivaer_data_full/" + vtp_output_path = f"/user_data/datasets/drivaer_aws/drivaer_data_full/{run_id}/" + + all_files = list(range(1, 501)) + + all_files = [stl_input_path + f"/run_{i}/boundary_{i}.vtp" for i in all_files] + + # Remove files that already exist in the output directory + filtered_files = [] + for file_path in all_files: + base_name = os.path.basename(file_path) + output_path = os.path.join(vtp_output_path, f"pred_{base_name}") + if not os.path.exists(output_path): + filtered_files.append(file_path) + all_files = filtered_files + + # print(f"all files: {all_files} of length {len(all_files)}") + + this_device_files = all_files[dist_manager.rank :: dist_manager.world_size] + + print( + f"Rank {dist_manager.rank} of {dist_manager.world_size} is processing {len(this_device_files)} files" + ) + for vtp_file in this_device_files: + start = time.time() + + # Process files: + process_vtp_file(vtp_file, model, norm_factors, dist_manager, vtp_output_path) + end = time.time() + print(f"time taken: {end - start}") + + +@hydra.main(version_base=None, config_path="conf", config_name="train_surface") +def launch(cfg: DictConfig): + """Launch training with hydra configuration + + Args: + cfg: Hydra configuration object + """ + inference_on_vtp_or_stl(cfg) + + +if __name__ == "__main__": + launch() diff --git a/examples/cfd/external_aerodynamics/transolver/inference_on_zarr.py b/examples/cfd/external_aerodynamics/transolver/inference_on_zarr.py new file mode 100644 index 0000000000..2e9549f10f --- /dev/null +++ b/examples/cfd/external_aerodynamics/transolver/inference_on_zarr.py @@ -0,0 +1,169 @@ +# SPDX-FileCopyrightText: Copyright (c) 2023 - 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-FileCopyrightText: All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import torch + +import hydra +from omegaconf import DictConfig +from physicsnemo.models.transolver.transolver import Transolver +from physicsnemo.launch.utils import load_checkpoint + +from physicsnemo.distributed import DistributedManager + +import time + +from datapipe import DomainParallelZarrDataset + +from train import forward_pass +from tabulate import tabulate + + +def inference(cfg: DictConfig) -> None: + """ + Run inference on a validation Zarr dataset using a trained Transolver model. + + Args: + cfg (DictConfig): Hydra configuration object containing model, data, and training settings. + + Returns: + None + """ + DistributedManager.initialize() + + dist_manager = DistributedManager() + + # Set up model + model = hydra.utils.instantiate(cfg.model) + model.eval() + model.to(dist_manager.device) + + # Validation dataset + + val_dataset = DomainParallelZarrDataset( + data_path=cfg.data.val.data_path, # Assuming validation data path is configured + device_mesh=None, + placements=None, + max_workers=cfg.data.max_workers, + pin_memory=cfg.data.pin_memory, + keys_to_read=cfg.data.data_keys, + large_keys=cfg.data.large_keys, + ) + + ckpt_args = { + "path": f"{cfg.output_dir}/{cfg.run_id}/checkpoints", + "models": model, + } + + # Load the normalization factors: + norm_file = "surface_fields_normalization.npz" + norm_data = np.load(norm_file) + norm_factors = { + "mean": torch.from_numpy(norm_data["mean"]).to(dist_manager.device), + "std": torch.from_numpy(norm_data["std"]).to(dist_manager.device), + } + + loaded_epoch = load_checkpoint(device=dist_manager.device, **ckpt_args) + print(f"loaded epoch: {loaded_epoch}") + + results = [] + start = time.time() + for batch_idx in range(len(val_dataset)): + + batch = val_dataset[batch_idx] + with torch.no_grad(): + loss, metrics = forward_pass( + batch, + model, + cfg.training.precision, + None, + dist_manager, + cfg, + norm_factors, + ) + + # Extract metric values and convert tensors to floats + l2_pressure = ( + metrics["l2_pressure"].item() + if hasattr(metrics["l2_pressure"], "item") + else metrics["l2_pressure"] + ) + l2_shear_x = ( + metrics["l2_shear_x"].item() + if hasattr(metrics["l2_shear_x"], "item") + else metrics["l2_shear_x"] + ) + l2_shear_y = ( + metrics["l2_shear_y"].item() + if hasattr(metrics["l2_shear_y"], "item") + else metrics["l2_shear_y"] + ) + l2_shear_z = ( + metrics["l2_shear_z"].item() + if hasattr(metrics["l2_shear_z"], "item") + else metrics["l2_shear_z"] + ) + + end = time.time() + elapsed = end - start + print(f"Finished batch {batch_idx} in {elapsed:.4f} seconds") + results.append( + [ + batch_idx, + f"{loss:.4f}", + f"{l2_pressure:.4f}", + f"{l2_shear_x:.4f}", + f"{l2_shear_y:.4f}", + f"{l2_shear_z:.4f}", + f"{elapsed:.4f}", + ] + ) + + start = time.time() + + headers = [ + "Batch", + "Loss", + "L2 Pressure", + "L2 Shear X", + "L2 Shear Y", + "L2 Shear Z", + "Elapsed (s)", + ] + print(tabulate(results, headers=headers, tablefmt="github")) + + # Calculate means for each metric (skip batch index) + if results: + # Convert string values back to float for mean calculation + arr = np.array(results)[:, 1:].astype(float) + means = arr.mean(axis=0) + mean_row = ["Mean"] + [f"{m:.4f}" for m in means] + print(tabulate([mean_row], headers=headers, tablefmt="github")) + + +@hydra.main(version_base=None, config_path="conf", config_name="train_surface") +def launch(cfg: DictConfig) -> None: + """ + Launch inference with Hydra configuration. + + Args: + cfg (DictConfig): Hydra configuration object. + """ + inference(cfg) + + +if __name__ == "__main__": + launch() diff --git a/examples/cfd/external_aerodynamics/transolver/loss.py b/examples/cfd/external_aerodynamics/transolver/loss.py index 37dd31d969..f81632f4da 100644 --- a/examples/cfd/external_aerodynamics/transolver/loss.py +++ b/examples/cfd/external_aerodynamics/transolver/loss.py @@ -38,7 +38,7 @@ def loss_fn( if mode == "surface": loss = loss_fn_surface(pred, target, "mse") elif mode == "volume": - loss = loss_fn_volume(pred, target, "rmse") + loss = loss_fn_volume(pred, target, "mse") # 100 * integral_loss_fn(pred, target, others["surface_areas"], others["surface_normals"], others["stream_velocity"]) return loss diff --git a/examples/cfd/external_aerodynamics/transolver/metrics.py b/examples/cfd/external_aerodynamics/transolver/metrics.py index 5275710f06..219d315825 100644 --- a/examples/cfd/external_aerodynamics/transolver/metrics.py +++ b/examples/cfd/external_aerodynamics/transolver/metrics.py @@ -60,6 +60,7 @@ def metrics_fn( others: dict[str, torch.Tensor], dm: DistributedManager, mode: str, + norm_factors: dict[str, torch.Tensor], ) -> dict[str, torch.Tensor]: """ Computes metrics for either surface or volume data. @@ -76,9 +77,9 @@ def metrics_fn( """ with torch.no_grad(): if mode == "surface": - metrics = metrics_fn_surface(pred, target, others, dm) + metrics = metrics_fn_surface(pred, target, others, dm, norm_factors) elif mode == "volume": - metrics = metrics_fn_volume(pred, target, others, dm) + metrics = metrics_fn_volume(pred, target, others, dm, norm_factors) else: raise ValueError(f"Unknown data mode: {mode}") @@ -91,6 +92,7 @@ def metrics_fn_volume( target: torch.Tensor, others: dict[str, torch.Tensor], dm: DistributedManager, + norm_factors: dict[str, torch.Tensor], ) -> dict[str, torch.Tensor]: """ Computes L2 volume metrics between prediction and target. @@ -104,8 +106,8 @@ def metrics_fn_volume( Returns: Dictionary of L2 volume metrics. """ - target = target * others["norm_std"] + others["norm_mean"] - pred = pred * others["norm_std"] + others["norm_mean"] + target = target * norm_factors["std"] + norm_factors["mean"] + pred = pred * norm_factors["std"] + norm_factors["mean"] l2_num = (pred - target) ** 2 l2_num = torch.sum(l2_num, dim=1) @@ -133,6 +135,7 @@ def metrics_fn_surface( target: torch.Tensor, others: dict[str, torch.Tensor], dm: DistributedManager, + norm_factors: dict[str, torch.Tensor], ) -> dict[str, torch.Tensor]: """ Computes L2 surface metrics between prediction and target. @@ -147,8 +150,8 @@ def metrics_fn_surface( Dictionary of L2 surface metrics. """ # Unnormalize the surface values for L2: - target = target * others["norm_std"] + others["norm_mean"] - pred = pred * others["norm_std"] + others["norm_mean"] + target = target * norm_factors["std"] + norm_factors["mean"] + pred = pred * norm_factors["std"] + norm_factors["mean"] l2_num = (pred - target) ** 2 l2_num = torch.sum(l2_num, dim=1) diff --git a/examples/cfd/external_aerodynamics/transolver/preprocess.py b/examples/cfd/external_aerodynamics/transolver/preprocess.py index 0121807e06..5c6185f915 100644 --- a/examples/cfd/external_aerodynamics/transolver/preprocess.py +++ b/examples/cfd/external_aerodynamics/transolver/preprocess.py @@ -23,6 +23,7 @@ @profile def preprocess_surface_data( batch: dict, + norm_factors: dict[str, torch.Tensor], ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, dict]: """ @@ -41,9 +42,7 @@ def preprocess_surface_data( ).to(torch.float32) # Normalize the surface fields: - norm_mean = targets.mean(dim=1) - norm_std = torch.sqrt(torch.mean((targets - norm_mean.unsqueeze(1)) ** 2, dim=1)) - targets = (targets - norm_mean) / norm_std + targets = (targets - norm_factors["mean"]) / norm_factors["std"] # If you want to use this, be sure to updat the # functional_dim value in your configuration @@ -84,21 +83,14 @@ def preprocess_surface_data( "surface_normals": normals, "stream_velocity": batch["stream_velocity"], "air_density": batch["air_density"], - "norm_mean": norm_mean, - "norm_std": norm_std, } - print( - f"node_features shape: {node_features.shape} nd placements: {node_features._spec.placements}" - ) - print( - f"embeddings shape: {embeddings.shape} nd placements: {embeddings._spec.placements}" - ) - print(f"targets shape: {targets.shape} nd placements: {targets._spec.placements}") + return node_features, embeddings, targets, others def preprocess_volume_data( batch: dict, + norm_factors: dict[str, torch.Tensor], ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, dict]: """ Preprocess the volumetric data. Right now, it's just @@ -120,16 +112,10 @@ def preprocess_volume_data( embeddings = torch.cat([mesh_centers, sdf], dim=-1) - # Normalize the surface fields: - norm_mean = targets.mean(dim=1) - # norm_std = targets.std(dim=1) - norm_std = torch.sqrt(torch.mean((targets - norm_mean.unsqueeze(1)) ** 2, dim=1)) - - targets = (targets - norm_mean) / norm_std + # Normalize the volume fields: + targets = (targets - norm_factors["mean"]) / norm_factors["std"] others = { - "norm_mean": norm_mean, - "norm_std": norm_std, "stream_velocity": batch["stream_velocity"], "air_density": batch["air_density"], } @@ -143,6 +129,11 @@ def downsample_surface( targets: torch.Tensor, num_keep=1024, ): + + if num_keep == -1: + features = features.unsqueeze(1).expand(1, embeddings.shape[1], -1) + return features, embeddings, targets + """ Downsample the surface data. We generate one set of indices, and use it to sample the same points from the features, embeddings, diff --git a/examples/cfd/external_aerodynamics/transolver/requirements.txt b/examples/cfd/external_aerodynamics/transolver/requirements.txt index e0414bda88..3d3f33a9b1 100644 --- a/examples/cfd/external_aerodynamics/transolver/requirements.txt +++ b/examples/cfd/external_aerodynamics/transolver/requirements.txt @@ -3,4 +3,6 @@ tabulate tensorboard termcolor einops -transformer_engine[pytorch] \ No newline at end of file +transformer_engine[pytorch] +zarr>=3.0 +zarrs \ No newline at end of file diff --git a/examples/cfd/external_aerodynamics/transolver/train.py b/examples/cfd/external_aerodynamics/transolver/train.py index 0a7702845a..42f1b8d48b 100644 --- a/examples/cfd/external_aerodynamics/transolver/train.py +++ b/examples/cfd/external_aerodynamics/transolver/train.py @@ -23,6 +23,7 @@ from omegaconf import DictConfig from torch.utils.tensorboard import SummaryWriter +import numpy as np from torch.distributed.fsdp import ( FullyShardedDataParallel as FSDP, @@ -34,7 +35,6 @@ from physicsnemo.launch.utils import load_checkpoint, save_checkpoint from physicsnemo.launch.logging import PythonLogger, RankZeroLoggingWrapper from physicsnemo.distributed import DistributedManager -from typing import Literal from physicsnemo.utils.profiling import profile, Profiler @@ -108,45 +108,54 @@ def forward_pass( output_pad_size: int | None, dist_manager: DistributedManager, cfg: DictConfig, + norm_factors: dict[str, torch.Tensor], ): """ Run the forward pass of the model for one batch, including metrics and loss calculation. """ if cfg.data.mode == "surface": - features, embeddings, targets, others = preprocess_surface_data(batch) + features, embeddings, targets, others = preprocess_surface_data( + batch, norm_factors + ) features, embeddings, targets = downsample_surface( features, embeddings, targets, cfg.data.resolution ) elif cfg.data.mode == "volume": - features, embeddings, targets, others = preprocess_volume_data(batch) + features, embeddings, targets, others = preprocess_volume_data( + batch, norm_factors + ) features, embeddings, targets = downsample_volume( features, embeddings, targets, cfg.data.resolution ) else: raise ValueError(f"Unknown data mode: {cfg.data.mode}") + # del batch + # Cast precisions: features, embeddings = cast_precisions(features, embeddings, precision) with get_autocast_context(precision): - print( - f"features shape: {features.shape} nd placements: {features._spec.placements}" - ) - print( - f"embeddings shape: {embeddings.shape} nd placements: {embeddings._spec.placements}" - ) - print( - f"targets shape: {targets.shape} nd placements: {targets._spec.placements}" - ) - print(f"cat shape: {torch.cat((features, embeddings), dim=-1).shape}") + + # For fp8, we may have to pad the inputs: + if precision == "float8": + fx_dim = features.shape[-1] + embeddings.shape[-1] + if fx_dim % 16 != 0: + pad_size = 16 - (fx_dim % 16) + features = torch.nn.functional.pad(features, (0, pad_size)) + fx_dim = features.shape[-1] + embeddings.shape[-1] + outputs = model(features, embeddings) + if output_pad_size is not None: # Remove the padded outputs: outputs = outputs[:, :, :-output_pad_size] loss = loss_fn(outputs, targets, others, cfg.data.mode) - metrics = metrics_fn(outputs, targets, others, dist_manager, cfg.data.mode) + metrics = metrics_fn( + outputs, targets, others, dist_manager, cfg.data.mode, norm_factors + ) return loss, metrics @@ -154,7 +163,7 @@ def forward_pass( @profile def train_epoch( dataloader, - sampler: torch.utils.data.Sampler | None, + sampler: torch.utils.data.Sampler, model: torch.nn.Module, output_pad_size: int | None, optimizer: torch.optim.Optimizer, @@ -164,6 +173,7 @@ def train_epoch( epoch: int, cfg: DictConfig, dist_manager: DistributedManager, + norm_factors: dict[str, torch.Tensor], scaler: GradScaler | None = None, ) -> float: """ @@ -171,7 +181,7 @@ def train_epoch( Args: dataloader (list[dict]): Training data loader - sampler (torch.utils.data.Sampler | None): Sampler for distributed or sequential sampling. + sampler (torch.utils.data.Sampler): Sampler for distributed or sequential sampling. model (torch.nn.Module): The neural network model to train. output_pad_size (int | None): Optional output padding size for lowest precisions (FP8). optimizer (torch.optim.Optimizer): Optimizer for model parameters. @@ -181,8 +191,8 @@ def train_epoch( epoch (int): Current epoch number. cfg (DictConfig): Hydra configuration object. dist_manager (DistributedManager): Distributed manager from physicsnemo. + norm_factors (dict[str, torch.Tensor]): Normalization factors for the data. scaler (GradScaler | None, optional): Gradient scaler for mixed precision training. - Returns: float: The average training loss for the epoch. """ @@ -196,16 +206,20 @@ def train_epoch( start_time = time.time() with Profiler(): for i, batch_idx in enumerate(epoch_indices): - - if i > 10: - break batch = dataloader[batch_idx] + # preload the next batch, if we're not on the last batch if i < epoch_len - 1 and sampler is not None: dataloader.preload(epoch_indices[i + 1]) loss, metrics = forward_pass( - batch, model, precision, output_pad_size, dist_manager, cfg + batch, + model, + precision, + output_pad_size, + dist_manager, + cfg, + norm_factors, ) optimizer.zero_grad() @@ -237,8 +251,10 @@ def train_epoch( start_time = end_time images_per_second = 1 / duration + mem_usage = torch.cuda.memory_reserved() / 1024**3 + logger.info( - f"Epoch {epoch} [{i}/{epoch_len}] Loss: {this_loss:.6f} Duration: {duration:.2f}s" + f"Epoch {epoch} [{i}/{epoch_len}] Loss: {this_loss:.6f} Duration: {duration:.2f}s Mem: {mem_usage:.2f}GB" ) if dist_manager.rank == 0: writer.add_scalar( @@ -282,6 +298,7 @@ def val_epoch( epoch: int, cfg: DictConfig, dist_manager: DistributedManager, + norm_factors: dict[str, torch.Tensor], ) -> float: """ Run validation for one epoch. @@ -296,7 +313,7 @@ def val_epoch( epoch (int): Current epoch number. cfg (DictConfig): Hydra configuration object. dist_manager (DistributedManager): Distributed manager instance. - + norm_factors (dict[str, torch.Tensor]): Normalization factors for the data. Returns: float: The average validation loss for the epoch. """ @@ -312,8 +329,6 @@ def val_epoch( start_time = time.time() with torch.no_grad(): # Disable gradient computation for i, batch_idx in enumerate(epoch_indices): - if i > 10: - break # Get data from batch batch = dataloader[batch_idx] @@ -322,7 +337,13 @@ def val_epoch( dataloader.preload(epoch_indices[i + 1]) loss, metrics = forward_pass( - batch, model, precision, output_pad_size, dist_manager, cfg + batch, + model, + precision, + output_pad_size, + dist_manager, + cfg, + norm_factors, ) if i == 0: @@ -397,7 +418,7 @@ def main(cfg: DictConfig): if cfg.training.precision == "float8": # we have to manipulate the output shape # to enable fp8 computations with transformer_engine. - # need the output to be divisible by 16. + # need the input and output to be divisible by 16. # if (cfg.model.embedding_dim + cfg.model.functional_dim) % 16 != 0: if cfg.model.out_dim % 16 != 0: @@ -409,6 +430,14 @@ def main(cfg: DictConfig): ) else: output_pad_size = None + if (cfg.model.functional_dim + cfg.model.embedding_dim) % 16 != 0: + input_pad_size = 16 - ( + (cfg.model.functional_dim + cfg.model.embedding_dim) % 16 + ) + cfg.model.functional_dim += input_pad_size + logger.info( + f"Padding input dimension to {cfg.model.functional_dim} and {cfg.model.embedding_dim} for fp8 autocast" + ) else: input_pad_size = None output_pad_size = None @@ -418,44 +447,11 @@ def main(cfg: DictConfig): model.to(dist_manager.device) - # Configure domain parallelism, if enabled: - domain_size = int(cfg.training.domain_parallelism) - - if domain_size > 1: - # You can use -1 to one axis to indicate that you want to use all the GPUs in that dimension. - mesh = dist_manager.initialize_mesh( - mesh_shape=(-1, domain_size), mesh_dim_names=("ddp", "domain") - ) - # This is a subset of all the GPUs, and will vary depending on the process. - # Think of this as slicing the global mesh along the domain axis. - # It will contain only the GPUs that this process is sharing data with. - domain_mesh = mesh["domain"] - ddp_mesh = mesh["ddp"] - placements = (Shard(1),) - else: - domain_mesh = None - ddp_mesh = None - placements = None - - if domain_size > 1: - # Instead of DDP, for sharding we use FSDP. It's possible to use FSDP in the DDP - # mode, but since it's not pure data parallel we have to me more careful. - - # First, distribute the model so that each GPU has the copy with DTensor weights: - model = distribute_module(model, domain_mesh) - - model = FSDP( - model, - device_mesh=mesh["ddp"], - sharding_strategy=ShardingStrategy.NO_SHARD, - ) - - elif dist_manager.world_size > 1: - model = torch.nn.parallel.DistributedDataParallel( - model, - device_ids=[dist_manager.local_rank], - output_device=dist_manager.device, - ) + model = torch.nn.parallel.DistributedDataParallel( + model, + device_ids=[dist_manager.local_rank], + output_device=dist_manager.device, + ) num_params = sum(p.numel() for p in model.parameters()) logger.info(f"Number of parameters: {num_params}") @@ -464,8 +460,6 @@ def main(cfg: DictConfig): train_dataset = DomainParallelZarrDataset( data_path=cfg.data.train.data_path, - device_mesh=domain_mesh, - placements=placements, max_workers=cfg.data.max_workers, pin_memory=cfg.data.pin_memory, keys_to_read=cfg.data.data_keys, @@ -476,20 +470,14 @@ def main(cfg: DictConfig): val_dataset = DomainParallelZarrDataset( data_path=cfg.data.val.data_path, # Assuming validation data path is configured - device_mesh=domain_mesh, - placements=placements, max_workers=cfg.data.max_workers, pin_memory=cfg.data.pin_memory, keys_to_read=cfg.data.data_keys, large_keys=cfg.data.large_keys, ) - if ddp_mesh is not None: - num_replicas = ddp_mesh.size() - data_rank = ddp_mesh.get_local_rank() - else: - num_replicas = dist_manager.world_size - data_rank = dist_manager.rank + num_replicas = dist_manager.world_size + data_rank = dist_manager.rank # Set up distributed samplers train_sampler = torch.utils.data.distributed.DistributedSampler( @@ -508,9 +496,16 @@ def main(cfg: DictConfig): drop_last=True, ) - print( - f"num_replicas: {num_replicas}, data_rank: {data_rank}, val targets: {list(val_sampler)}" - ) + # Load the normalization file: + if cfg.data.mode == "surface": + norm_file = "surface_fields_normalization.npz" + elif cfg.data.mode == "volume": + norm_file = "volume_fields_normalization.npz" + norm_data = np.load(norm_file) + norm_factors = { + "mean": torch.from_numpy(norm_data["mean"]).to(dist_manager.device), + "std": torch.from_numpy(norm_data["std"]).to(dist_manager.device), + } # Set up optimizer and scheduler optimizer = hydra.utils.instantiate(cfg.optimizer, params=model.parameters()) @@ -544,6 +539,7 @@ def main(cfg: DictConfig): "scheduler": scheduler, "models": model, } + loaded_epoch = load_checkpoint(device=dist_manager.device, **ckpt_args) if cfg.training.compile: @@ -570,6 +566,7 @@ def main(cfg: DictConfig): epoch, cfg, dist_manager, + norm_factors, scaler, ) end_time = time.time() @@ -587,6 +584,7 @@ def main(cfg: DictConfig): epoch, cfg, dist_manager, + norm_factors, ) end_time = time.time() val_duration = end_time - start_time @@ -613,9 +611,12 @@ def launch(cfg: DictConfig): Args: cfg: Hydra configuration object """ + + # If you want to use `line_profiler` or PyTorch's profiler, enable them here. + profiler = Profiler() # profiler.enable("torch") - profiler.enable("line_profiler") + # profiler.enable("line_profiler") profiler.initialize() main(cfg) profiler.finalize() diff --git a/physicsnemo/models/transolver/transolver.py b/physicsnemo/models/transolver/transolver.py index 129314411e..45b6410fea 100644 --- a/physicsnemo/models/transolver/transolver.py +++ b/physicsnemo/models/transolver/transolver.py @@ -385,7 +385,7 @@ def __init__( n_layers=0, res=False, act=act, - use_te=False, + use_te=use_te, ) self.Time_Input = Time_Input From 4f4906a8692d44dda3b63a8a154ca8cc56be0abf Mon Sep 17 00:00:00 2001 From: Corey Adams Date: Thu, 31 Jul 2025 13:33:17 -0700 Subject: [PATCH 21/23] Ensure embedding is not None before concat --- physicsnemo/models/transolver/transolver.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/physicsnemo/models/transolver/transolver.py b/physicsnemo/models/transolver/transolver.py index 6fcd0f5998..cc60ccba23 100644 --- a/physicsnemo/models/transolver/transolver.py +++ b/physicsnemo/models/transolver/transolver.py @@ -507,6 +507,9 @@ def forward( embedding = embedding.reshape( embedding.shape[0], *self.structured_shape, -1 ) + else: + if embedding is None: + raise ValueError("Embedding is required for unstructured data") if self.unified_pos: # Extend the embedding to the batch size: @@ -514,13 +517,10 @@ def forward( self.embedding.repeat(embedding.shape[0], 1, 1) # .reshape(x.shape[0], -1, self.embedding_dim) ) + # Combine the embedding and functional input: - fx = torch.cat((embedding, fx), -1) - # Dead code? Not sure when this is used. - # if fx is not None: - # else: - # fx = self.preprocess(x) - # fx = fx + self.placeholder[None, None, :] + if embedding is not None: + fx = torch.cat((embedding, fx), -1) # Apply preprocessing fx = self.preprocess(fx) From ea6c2c91ecf34552a3bbeb2e2cbee84dd386a83b Mon Sep 17 00:00:00 2001 From: Corey Adams Date: Thu, 31 Jul 2025 13:41:45 -0700 Subject: [PATCH 22/23] Update transolver for minor details. --- physicsnemo/models/transolver/transolver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/physicsnemo/models/transolver/transolver.py b/physicsnemo/models/transolver/transolver.py index cc60ccba23..a011bc006e 100644 --- a/physicsnemo/models/transolver/transolver.py +++ b/physicsnemo/models/transolver/transolver.py @@ -286,7 +286,7 @@ class Transolver(Module): that the number of heads must evenly divide the `n_hidden` parameter to yield an integer head dimension. act : str - The activation function, default is Gelu. + The activation function, default is gelu. mlp_ratio : int The ratio of hidden dimension in the MLP, default is 4. Used in the MLPs in the PhysicsAttention Layers. @@ -481,7 +481,7 @@ def forward( self, fx: torch.Tensor | None, embedding: torch.Tensor | None = None, - T: torch.Tensor = None, + T: torch.Tensor | None = None, ) -> torch.Tensor: """ Forward pass of the transolver model. From 9fcd89ced6cd35e70af199f9b5b27aa5fe1556f0 Mon Sep 17 00:00:00 2001 From: Corey Adams Date: Fri, 1 Aug 2025 00:46:41 +0000 Subject: [PATCH 23/23] Update transolver tests, clean up lingering details in the model. --- .../models/transolver/Physics_Attention.py | 26 +-- physicsnemo/models/transolver/transolver.py | 11 +- test/models/data/transolver2d_output.pth | Bin 0 -> 117033 bytes .../data/transolver_irregular_output.pth | Bin 0 -> 199129 bytes .../data/transolver_irregular_te_output.pth | Bin 0 -> 199147 bytes test/models/data/transolver_output.pth | Bin 116814 -> 0 bytes test/models/test_transolver.py | 220 ++++++++++++------ 7 files changed, 162 insertions(+), 95 deletions(-) create mode 100644 test/models/data/transolver2d_output.pth create mode 100644 test/models/data/transolver_irregular_output.pth create mode 100644 test/models/data/transolver_irregular_te_output.pth delete mode 100644 test/models/data/transolver_output.pth diff --git a/physicsnemo/models/transolver/Physics_Attention.py b/physicsnemo/models/transolver/Physics_Attention.py index f6571ff37f..5f4b5746eb 100644 --- a/physicsnemo/models/transolver/Physics_Attention.py +++ b/physicsnemo/models/transolver/Physics_Attention.py @@ -74,9 +74,7 @@ def __init__(self, dim, heads, dim_head, dropout, slice_num, use_te): for l_i in [self.in_project_slice]: torch.nn.init.orthogonal_(l_i.weight) # use a principled initialization if not use_te: - self.to_q = nn.Linear(dim_head, dim_head, bias=False) - self.to_k = nn.Linear(dim_head, dim_head, bias=False) - self.to_v = nn.Linear(dim_head, dim_head, bias=False) + self.qkv_project = nn.Linear(dim_head, 3 * dim_head, bias=False) else: # These are used in the transformer engine pass function: self.qkv_project = te.Linear(dim_head, 3 * dim_head, bias=False) @@ -187,12 +185,10 @@ def compute_slice_attention_sdpa(self, slice_tokens: torch.Tensor) -> torch.Tens Torch SDPA implementation of slice attention """ - # qkv = self.qkv_project(slice_tokens) - # qkv = rearrange(qkv, " b h s (t d) -> t b h s d", t=3, d=self.dim_head) - q_slice_token = self.to_q(slice_tokens) - k_slice_token = self.to_k(slice_tokens) - v_slice_token = self.to_v(slice_tokens) - # q_slice_token, k_slice_token, v_slice_token = qkv.unbind(0) + qkv = self.qkv_project(slice_tokens) + qkv = rearrange(qkv, " b h s (t d) -> t b h s d", t=3, d=self.dim_head) + + q_slice_token, k_slice_token, v_slice_token = qkv.unbind(0) out_slice_token3 = torch.nn.functional.scaled_dot_product_attention( q_slice_token, k_slice_token, v_slice_token, is_causal=True @@ -255,7 +251,6 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: if self.use_te: out_slice_token = self.compute_slice_attention_te(slice_tokens) else: - # out_slice_token = self.compute_slice_attention(slice_tokens) out_slice_token = self.compute_slice_attention_sdpa(slice_tokens) # Shape unchanged @@ -263,8 +258,6 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: # Deslice: outputs = self.project_attention_outputs(out_slice_token, slice_weights) - # print(f"outputs mean and shape: {outputs.mean()} and {outputs.shape}") - # Outputs now has the same shape as the original input x return outputs @@ -280,8 +273,12 @@ def __init__( ): super().__init__(dim, heads, dim_head, dropout, slice_num, use_te) inner_dim = dim_head * heads - self.in_project_x = nn.Linear(dim, inner_dim) - self.in_project_fx = nn.Linear(dim, inner_dim) + if use_te: + self.in_project_x = te.Linear(dim, inner_dim) + self.in_project_fx = te.Linear(dim, inner_dim) + else: + self.in_project_x = nn.Linear(dim, inner_dim) + self.in_project_fx = nn.Linear(dim, inner_dim) def project_input_onto_slices(self, x) -> tuple[torch.Tensor, torch.Tensor]: """ @@ -323,7 +320,6 @@ def __init__( self.in_project_fx = nn.Conv2d(dim, inner_dim, kernel, 1, kernel // 2) def project_input_onto_slices(self, x) -> tuple[torch.Tensor, torch.Tensor]: - # Rearrange the input tokens back to an image shape: x = rearrange(x, "b (h w) c -> b c h w", h=self.H, w=self.W) diff --git a/physicsnemo/models/transolver/transolver.py b/physicsnemo/models/transolver/transolver.py index a011bc006e..88a5bc1ad1 100644 --- a/physicsnemo/models/transolver/transolver.py +++ b/physicsnemo/models/transolver/transolver.py @@ -220,7 +220,7 @@ class MetaData(ModelMetaData): # Optimization jit: bool = False cuda_graphs: bool = False - amp: bool = False + amp: bool = True # Inference onnx_cpu: bool = False # No FFT op on CPU onnx_gpu: bool = True @@ -423,9 +423,13 @@ def initialize_weights(self): self.apply(self._init_weights) def _init_weights(self, m): - if isinstance(m, nn.Linear): + linear_layers = (nn.Linear,) + if self.use_te: + linear_layers = linear_layers + (te.Linear,) + + if isinstance(m, linear_layers): nn.init.trunc_normal_(m.weight, std=0.02) - if isinstance(m, nn.Linear) and m.bias is not None: + if isinstance(m, linear_layers) and m.bias is not None: nn.init.constant_(m.bias, 0) norm_layers = (nn.LayerNorm, nn.BatchNorm1d) if self.use_te: @@ -496,7 +500,6 @@ def forward( """ with torch.autograd.profiler.record_function("preprocess"): - # Reshape automatically, if necessary: if self.structured_shape is not None: unflatten_output = False diff --git a/test/models/data/transolver2d_output.pth b/test/models/data/transolver2d_output.pth new file mode 100644 index 0000000000000000000000000000000000000000..031955ce2ad5e1b8d2ac3866e1ccec91a71acbbe GIT binary patch literal 117033 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 z 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�vl9 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 N!CU?Q{r~#i{{VRXqJ{tf literal 0 HcmV?d00001 diff --git a/test/models/data/transolver_irregular_output.pth b/test/models/data/transolver_irregular_output.pth new file mode 100644 index 0000000000000000000000000000000000000000..8f3d852e6be77c49d46939f84a82c4c60c8188e3 GIT binary patch literal 199129 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 literal 0 HcmV?d00001 diff --git a/test/models/data/transolver_irregular_te_output.pth b/test/models/data/transolver_irregular_te_output.pth new file mode 100644 index 0000000000000000000000000000000000000000..ce9c394c0b592eb5763e0cb529e196799fe12fd1 GIT binary patch literal 199147 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 &dq5i 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%0A{QW~>l~bZeJ~son>5{{$@iS&2)%rj+XeUna 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

H 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,U1t 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 OOjrKr@B9Dlz5few;fy~3 diff --git a/test/models/test_transolver.py b/test/models/test_transolver.py index 8b29e78e82..b6775f7f61 100644 --- a/test/models/test_transolver.py +++ b/test/models/test_transolver.py @@ -18,19 +18,29 @@ import pytest import torch +from common import ( + check_ort_version, + validate_amp, + validate_checkpoint, + validate_combo_optims, + validate_cuda_graphs, + validate_forward_accuracy, + validate_jit, + validate_onnx_export, + validate_onnx_runtime, +) +from pytest_utils import import_or_fail from physicsnemo.models.transolver import Transolver -from . import common - @pytest.mark.parametrize("device", ["cuda:0", "cpu"]) -def test_transolver_forward(device): - """Test FNO forward pass""" +def test_transolver2d_forward(device): + """Test Transolver2D forward pass""" torch.manual_seed(0) - # Construct FNO model + # Construct Transolver model model = Transolver( - space_dim=2, + structured_shape=(85, 85), n_layers=8, n_hidden=64, dropout=0, @@ -38,36 +48,42 @@ def test_transolver_forward(device): Time_Input=False, act="gelu", mlp_ratio=1, - fun_dim=1, + functional_dim=1, out_dim=1, slice_num=32, - ref=8, - unified_pos=1, - H=85, - W=85, + ref=1, + unified_pos=True, + use_te=False, ).to(device) bsize = 4 - pos = torch.randn(bsize, 85, 85).to(device) - invar = torch.randn(bsize, 85 * 85, 1).to(device) - assert common.validate_forward_accuracy( + fx = torch.randn(bsize, 85 * 85, 1).to(device) + embedding = torch.randn(bsize, 85, 85).to(device) + + print(f"fx: {fx.shape}") + print(f"embedding: {embedding.shape}") + + print(f"output shape: {model(fx, embedding).shape}") + + assert validate_forward_accuracy( model, ( - pos, - invar, + fx, + embedding, ), - file_name="transolver_output.pth", + file_name="transolver2d_output.pth", atol=1e-3, ) @pytest.mark.parametrize("device", ["cuda:0", "cpu"]) -def test_transolver_constructor(device): - """Test transolver constructor options""" - # Define dictionary of constructor args +def test_transolver_irregular_forward(device): + """Test Transolver Irregular forward pass""" + torch.manual_seed(0) + # Construct Transolver model model = Transolver( - space_dim=2, + structured_shape=None, n_layers=8, n_hidden=64, dropout=0, @@ -75,31 +91,40 @@ def test_transolver_constructor(device): Time_Input=False, act="gelu", mlp_ratio=1, - fun_dim=1, + functional_dim=2, + embedding_dim=3, out_dim=1, slice_num=32, - ref=8, - unified_pos=1, - H=85, - W=85, + ref=1, + unified_pos=False, + use_te=False, ).to(device) - bsize = random.randint(1, 4) - pos = torch.randn(bsize, 85, 85).to(device) - invar = torch.randn(bsize, 85 * 85, 1).to(device) + bsize = 4 + + embedding = torch.randn(bsize, 12345, 3).to(device) + functional_input = torch.randn(bsize, 12345, 2).to(device) - outvar = model(pos, invar) - assert outvar.shape == (bsize, 85, 85, 1) + assert validate_forward_accuracy( + model, + ( + embedding, + functional_input, + ), + file_name="transolver_irregular_output.pth", + atol=1e-3, + ) -@pytest.mark.parametrize("device", ["cuda:0", "cpu"]) +@pytest.mark.parametrize("device", ["cuda:0"]) def test_transolver_optims(device): """Test transolver optimizations""" def setup_model(): """Setups up fresh transolver model and inputs for each optim test""" + model = Transolver( - space_dim=2, + structured_shape=None, n_layers=8, n_hidden=64, dropout=0, @@ -107,24 +132,25 @@ def setup_model(): Time_Input=False, act="gelu", mlp_ratio=1, - fun_dim=1, + functional_dim=2, + embedding_dim=3, out_dim=1, slice_num=32, - ref=8, - unified_pos=1, - H=85, - W=85, + ref=1, + unified_pos=False, + use_te=False, ).to(device) - bsize = random.randint(1, 2) - pos = torch.randn(bsize, 85, 85).to(device) - invar = torch.randn(bsize, 85 * 85, 1).to(device) + bsize = 4 + + embedding = torch.randn(bsize, 12345, 3).to(device) + functional_input = torch.randn(bsize, 12345, 2).to(device) - return model, pos, invar + return model, embedding, functional_input # Ideally always check graphs first model, pos, invar = setup_model() - assert common.validate_cuda_graphs( + assert validate_cuda_graphs( model, ( pos, @@ -134,7 +160,7 @@ def setup_model(): # Check JIT model, pos, invar = setup_model() - assert common.validate_jit( + assert validate_jit( model, ( pos, @@ -143,7 +169,7 @@ def setup_model(): ) # Check AMP model, pos, invar = setup_model() - assert common.validate_amp( + assert validate_amp( model, ( pos, @@ -152,7 +178,7 @@ def setup_model(): ) # Check Combo model, pos, invar = setup_model() - assert common.validate_combo_optims( + assert validate_combo_optims( model, ( pos, @@ -161,12 +187,53 @@ def setup_model(): ) +@import_or_fail("transformer_engine") +def test_transolver_te(pytestconfig): + if not torch.cuda.is_available(): + pytest.skip("CUDA is not available") + + torch.manual_seed(0) + + model = Transolver( + structured_shape=None, + n_layers=8, + n_hidden=64, + dropout=0, + n_head=4, + Time_Input=False, + act="gelu", + mlp_ratio=1, + functional_dim=2, + embedding_dim=3, + out_dim=1, + slice_num=32, + ref=1, + unified_pos=False, + use_te=True, + ).to("cuda") + + bsize = 4 + + embedding = torch.randn(bsize, 12345, 3).to("cuda") + functional_input = torch.randn(bsize, 12345, 2).to("cuda") + + assert validate_forward_accuracy( + model, + ( + embedding, + functional_input, + ), + file_name="transolver_irregular_te_output.pth", + atol=1e-3, + ) + + @pytest.mark.parametrize("device", ["cuda:0", "cpu"]) def test_transolver_checkpoint(device): """Test transolver checkpoint save/load""" # Construct transolver models model_1 = Transolver( - space_dim=2, + structured_shape=None, n_layers=8, n_hidden=64, dropout=0, @@ -174,17 +241,17 @@ def test_transolver_checkpoint(device): Time_Input=False, act="gelu", mlp_ratio=1, - fun_dim=1, + functional_dim=2, + embedding_dim=3, out_dim=1, slice_num=32, - ref=8, - unified_pos=1, - H=85, - W=85, + ref=1, + unified_pos=False, + use_te=False, ).to(device) model_2 = Transolver( - space_dim=2, + structured_shape=None, n_layers=8, n_hidden=64, dropout=0, @@ -192,36 +259,37 @@ def test_transolver_checkpoint(device): Time_Input=False, act="gelu", mlp_ratio=1, - fun_dim=1, + functional_dim=2, + embedding_dim=3, out_dim=1, slice_num=32, - ref=8, - unified_pos=1, - H=85, - W=85, + ref=1, + unified_pos=False, + use_te=False, ).to(device) bsize = random.randint(1, 2) - pos = torch.randn(bsize, 85, 85).to(device) - invar = torch.randn(bsize, 85 * 85, 1).to(device) - assert common.validate_checkpoint( + embedding = torch.randn(bsize, 12345, 3).to(device) + functional_input = torch.randn(bsize, 12345, 2).to(device) + + assert validate_checkpoint( model_1, model_2, ( - pos, - invar, + functional_input, + embedding, ), ) -@common.check_ort_version() +@check_ort_version() @pytest.mark.parametrize("device", ["cuda:0", "cpu"]) -def test_transolverdeploy(device): +def test_transolver_deploy(device): """Test transolver deployment support""" # Construct transolver model model = Transolver( - space_dim=2, + structured_shape=(85, 85), n_layers=8, n_hidden=64, dropout=0, @@ -229,27 +297,27 @@ def test_transolverdeploy(device): Time_Input=False, act="gelu", mlp_ratio=1, - fun_dim=1, + functional_dim=1, out_dim=1, slice_num=32, - ref=8, - unified_pos=1, - H=85, - W=85, + ref=1, + unified_pos=True, + use_te=False, ).to(device) - bsize = random.randint(1, 2) - pos = torch.randn(bsize, 85, 85).to(device) - invar = torch.randn(bsize, 85 * 85, 1).to(device) + bsize = 4 + + pos = torch.randn(bsize, 85 * 85, 1).to(device) + invar = torch.randn(bsize, 85, 85).to(device) - assert common.validate_onnx_export( + assert validate_onnx_export( model, ( pos, invar, ), ) - assert common.validate_onnx_runtime( + assert validate_onnx_runtime( model, ( invar,