steady-rans-surrogates / code /ezflow_v3 /baselines /transolverpp_wrap.py
BlidReview's picture
weights, code, eval script
bdce880 verified
Raw
History Blame Contribute Delete
5.18 kB
"""Wrapper around the OFFICIAL Transolver++ model (thuml/Transolver_plus, ICML 2025)
so it trains/evaluates in our harness on our PyG graphs, apples to apples with GeoReNet
and the Transolver baseline (same data, split, loss, eval). We load the authors'
models/Transolver_plus.py verbatim and only adapt the I/O plus two environment shims:
1. timm: the official file does `from timm.layers import trunc_normal_`; we stub that
one symbol with torch's native trunc_normal_ (avoids the timm dependency that
conflicts with our pinned torch 2.11+cu128).
2. torch.distributed.nn.all_reduce: the eidetic attention sums slice statistics across
GPUs via all_reduce (the paper's input-invariant parallelism). On a single GPU,
all_reduce over one process is the identity, so we replace it with a passthrough.
This is the faithful single-GPU behavior of the authors' code, not a model change.
The Model.forward consumes (x, pos, condition): x = [coords + node features], pos = coords
(used only if unified_pos), condition = optional global token. We feed coords + node
features + broadcast (Re, yaw) globals so Transolver++ gets the SAME conditioning signal
GeoReNet's FiLM uses (fair), and condition=None.
"""
from __future__ import annotations
import os, sys, types, importlib.util
import torch
import torch.nn as nn
_OFFICIAL = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"Transolver_plus-main", "models", "Transolver_plus.py")
def _stub_timm():
if "timm.layers" in sys.modules:
return
timm = types.ModuleType("timm")
layers = types.ModuleType("timm.layers"); layers.trunc_normal_ = torch.nn.init.trunc_normal_
models = types.ModuleType("timm.models")
mlayers = types.ModuleType("timm.models.layers"); mlayers.trunc_normal_ = torch.nn.init.trunc_normal_
timm.layers = layers; timm.models = models; models.layers = mlayers
sys.modules.update({"timm": timm, "timm.layers": layers,
"timm.models": models, "timm.models.layers": mlayers})
def _load_official():
_stub_timm()
spec = importlib.util.spec_from_file_location("transolverpp_official", _OFFICIAL)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
# single-GPU shims: ensure ReduceOp exists, then make all_reduce the identity
# (all_reduce over one process is a passthrough = faithful single-GPU behavior).
import torch.distributed as _td
if not hasattr(mod.dist_nn, "ReduceOp"):
mod.dist_nn.ReduceOp = getattr(_td, "ReduceOp", types.SimpleNamespace(SUM=0))
mod.dist_nn.all_reduce = lambda t, op=None: t
return mod
class _NS:
pass
class TransolverPlusWrapper(nn.Module):
"""Drop-in for our training loop: forward(data) -> [N_nodes, out_dim].
Config sized to ~GeoReNet/Transolver scale for a fair comparison; align to the
authors' main_airplane.py settings if exact faithfulness to their aero run is wanted."""
def __init__(self, node_in, space_dim=3, global_dim=2, out_dim=7, n_hidden=256,
n_layers=8, n_head=8, slice_num=64, mlp_ratio=2):
super().__init__()
mod = _load_official()
self.space_dim = space_dim
self.global_dim = global_dim
# Model preprocess input = fun_dim + space_dim; we put coords in space_dim and
# node features + broadcast globals in fun_dim.
self.net = mod.Model(space_dim=space_dim, fun_dim=node_in + global_dim, out_dim=out_dim,
n_hidden=n_hidden, n_layers=n_layers, n_head=n_head,
slice_num=slice_num, mlp_ratio=mlp_ratio, unified_pos=False)
def forward(self, data):
# PER GRAPH: a PyG DataLoader concatenates graphs into one node tensor with a
# `batch` vector. Transolver++ attention treats its input as ONE point cloud, so
# we must run each graph separately (else nodes leak across graphs and only graph
# 0's globals are used). Output is reassembled in the original node order.
N = data.x.shape[0]
batch = getattr(data, "batch", None)
if batch is None:
batch = torch.zeros(N, dtype=torch.long, device=data.x.device)
B = int(batch.max().item()) + 1
pos_all = data.pos
if pos_all.shape[1] < self.space_dim:
pos_all = torch.cat([pos_all, pos_all.new_zeros(pos_all.shape[0],
self.space_dim - pos_all.shape[1])], 1)
gf_all = data.global_feat.reshape(B, -1)[:, :self.global_dim] if self.global_dim else None
outs = []
for b in range(B):
m = batch == b
pos = pos_all[m]
parts = [pos[:, :self.space_dim], data.x[m]]
if self.global_dim:
parts.append(gf_all[b:b + 1].expand(pos.shape[0], -1))
x = torch.cat(parts, dim=-1).unsqueeze(0) # [1, n_b, space+fun]
outs.append(self.net((x, pos.unsqueeze(0), None))[0]) # [n_b, out_dim]
return torch.cat(outs, dim=0) # [N, out_dim], original order
def count_params(m):
return sum(p.numel() for p in m.parameters())