2090 lines
90 KiB
Python
2090 lines
90 KiB
Python
import torch
|
||
import torch.nn as nn
|
||
import torch.optim as optim
|
||
from torch.optim.lr_scheduler import StepLR, ReduceLROnPlateau
|
||
from torch.utils.checkpoint import checkpoint # 梯度检查点,节省显存
|
||
from datetime import timedelta # 用于DDP超时设置
|
||
import matplotlib.pyplot as plt
|
||
# 混合精度训练已禁用,使用纯float32训练以保证稳定性
|
||
from torch_geometric.loader import DataListLoader # 【关键】使用 ListLoader
|
||
import torch.distributed as dist # DDP 分布式训练
|
||
import torch.multiprocessing as mp # 多进程
|
||
from torch.nn.parallel import DistributedDataParallel as DDP # DDP
|
||
from torch_geometric.nn import DataParallel # 兼容性保留
|
||
from torch_geometric.data import Batch, Data
|
||
import numpy as np
|
||
import os
|
||
import time
|
||
import copy
|
||
import json
|
||
from sklearn.model_selection import train_test_split
|
||
|
||
# ==========================================
|
||
# 1. 导入自定义模块
|
||
# ==========================================
|
||
from build_graph import build_graph_data, scan_all_data, root_data_path, load_file_data, is_main_process
|
||
from model import BuildGCNList
|
||
import os
|
||
# DDP环境优化:避免多进程数据加载导致的资源竞争
|
||
# 在DDP中,每个GPU进程都会创建num_workers个子进程
|
||
# 如果有N个GPU,num_workers=4,则总共有4*N个数据加载子进程
|
||
# 这会导致文件描述符耗尽和共享内存对象过多
|
||
# 建议:DDP环境中设置num_workers=0,让主进程处理数据加载
|
||
NUM_WORKERS = 0 # DDP环境:禁用多进程数据加载以避免资源竞争
|
||
|
||
# ==========================================
|
||
# 2. 训练配置
|
||
# ==========================================
|
||
# 学习率调度配置
|
||
# ==========================================
|
||
# ReduceLROnPlateau 配置
|
||
REDUCE_LR_PATIENCE = 30 # 多少个epoch无改善就降低学习率
|
||
REDUCE_LR_FACTOR = 0.5 # 学习率降低因子
|
||
REDUCE_LR_MIN_LR = 1e-6 # 最小学习率
|
||
REDUCE_LR_MODE = 'min' # 'min'表示监控指标越小越好
|
||
# REDUCE_LR_VERBOSE 已弃用,使用 get_last_lr() 替代
|
||
|
||
# ==========================================
|
||
# 早停机制配置
|
||
# ==========================================
|
||
EARLY_STOPPING_PATIENCE = 50 # 连续多少个epoch无改善就停止训练
|
||
EARLY_STOPPING_MIN_DELTA = 1e-5 # 最小改善阈值 (0.001e-2 = 1e-5)
|
||
EARLY_STOPPING_START_EPOCH = 10 # 从第几个epoch开始检查早停(给模型预热时间)
|
||
# DP 模式下 batch_size 是所有卡的总和
|
||
# 例如:3张卡,batch_size=48 -> 每张卡分到 16 个图
|
||
# 增加 batch size 以提高 GPU 利用率
|
||
MATRIX_CACHE = {}
|
||
# 优化:DDP模式下每个GPU的batch size
|
||
# DDP性能优化:增大batch_size以更好地利用GPU并行计算
|
||
# - 24GB 显存:建议 128-256 per GPU
|
||
# - 48GB 显存:建议 256-512 per GPU
|
||
# - 如果遇到OOM,可以适当减小
|
||
TOTAL_BATCH_SIZE = 64
|
||
EPOCH_ADAM = 2000 # 只使用Adam优化器
|
||
TOTAL_EPOCHS = EPOCH_ADAM
|
||
EPOCH_PRINT = 50
|
||
# 从全局配置文件导入参数
|
||
from config import N_ITER, SAVE_DIR, OUTPUT_DIR, LOSS_TYPE, MASTER_PORT, LOAD_PRETRAINED_MODEL, PRETRAINED_MODEL_DIR
|
||
|
||
# ==========================================
|
||
# 1.5. 输出目录配置
|
||
# ==========================================
|
||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||
|
||
# 移除模块级别的打印,避免DDP重复打印
|
||
|
||
LR = 0.001
|
||
|
||
# 性能优化配置
|
||
USE_AMP = False # 禁用混合精度训练(使用纯float32以保证稳定性)
|
||
# torch.compile 配置说明:
|
||
# - 需要 PyTorch 2.0+ 支持
|
||
# - 可能提升 10-20% 前向传播速度
|
||
# - 首次运行需要编译时间(可能较慢)
|
||
# - 可能与 DataParallel 和梯度检查点有兼容性问题
|
||
# - 建议:先测试是否正常工作,如果遇到错误可以禁用
|
||
USE_COMPILE = False # 启用torch.compile可提升10-20%速度(需要PyTorch 2.0+)
|
||
PIN_MEMORY = True # 启用pin_memory以加速数据传输
|
||
USE_GRADIENT_CHECKPOINTING = False # 启用梯度检查点,节省显存(会稍微降低速度,约20-30%)
|
||
|
||
# ==========================================
|
||
# 3. 全局矩阵缓存 (Multi-Device Matrix Cache)
|
||
# ==========================================
|
||
# 用于解决 DP 模式下不同显卡需要访问不同设备上矩阵的问题
|
||
|
||
def load_matrix_to_cache(data_mapping, n_total, device_ids, is_main_process=True):
|
||
"""
|
||
将所有矩阵预加载到 CPU 上(使用 pin_memory 加速后续传输)。
|
||
结构: MATRIX_CACHE[k_idx] = (A_cpu, b_cpu)
|
||
|
||
优化:矩阵存储在 CPU,需要时才转移到 GPU,释放显存空间。
|
||
使用 pin_memory() 加速 CPU 到 GPU 的传输。
|
||
|
||
注意: device_ids 参数保留用于兼容性,但不再为每个 GPU 创建副本
|
||
"""
|
||
if is_main_process:
|
||
print(f"正在将矩阵预加载到 CPU(使用 pin_memory 优化)...")
|
||
print(f" 矩阵将在需要时动态传输到 GPU,以释放显存空间")
|
||
valid_indices = [k for k in range(1, n_total + 1) if k in data_mapping]
|
||
|
||
for idx, k_idx in enumerate(valid_indices):
|
||
folder_path, folder_num, data_id = data_mapping[k_idx]
|
||
try:
|
||
# 读取原始数据 (CPU)
|
||
Aij = load_file_data(folder_path, "Aij", folder_num, data_id)
|
||
Av = load_file_data(folder_path, "Av", folder_num, data_id)
|
||
b_data = load_file_data(folder_path, "b", folder_num, data_id)
|
||
|
||
rows = Aij[:, 0].astype(int) - 1
|
||
cols = Aij[:, 1].astype(int) - 1
|
||
values = Av[:, 0] + 1j * Av[:, 1]
|
||
N_nodes = len(b_data)
|
||
b_val = b_data[:, 0] + 1j * b_data[:, 1]
|
||
|
||
shape = (N_nodes, N_nodes)
|
||
|
||
# 在 CPU 上创建张量,并使用 pin_memory 加速后续传输
|
||
i = torch.from_numpy(np.vstack((rows, cols))).long()
|
||
# v = torch.from_numpy(values.astype(np.complex128)) # 使用双精度complex
|
||
# b_k = torch.from_numpy(b_val.astype(np.complex128)) # 使用双精度complex
|
||
# 改为:
|
||
v = torch.from_numpy(values.astype(np.complex64)) # 32位复数
|
||
b_k = torch.from_numpy(b_val.astype(np.complex64))
|
||
|
||
# # 将A矩阵和b向量都放大10倍
|
||
# v = v * 10
|
||
# b_k = b_k * 10
|
||
|
||
# 使用 pin_memory() 将数据固定在内存中,加速 CPU->GPU 传输
|
||
# 注意:稀疏矩阵的 indices 和 values 可以 pin_memory
|
||
i = i.pin_memory()
|
||
v = v.pin_memory()
|
||
b_k = b_k.pin_memory()
|
||
|
||
# 在 CPU 上创建稀疏矩阵(不 coalesce,延迟到 GPU 传输时)
|
||
# 存储 indices 和 values(已 pin_memory)以及 shape,而不是完整的稀疏矩阵
|
||
# 这样可以保持 pin_memory 状态
|
||
MATRIX_CACHE[k_idx] = {
|
||
'indices': i,
|
||
'values': v,
|
||
'shape': shape,
|
||
'b': b_k
|
||
}
|
||
|
||
if is_main_process and (idx + 1) % 1000 == 0:
|
||
print(f" 已缓存 {idx + 1}/{len(valid_indices)} 个样本")
|
||
|
||
except Exception as e:
|
||
if is_main_process:
|
||
print(f"加载样本 {k_idx} 出错: {e}")
|
||
continue
|
||
if is_main_process:
|
||
print("矩阵缓存完成(存储在 CPU,使用 pin_memory 优化,双精度complex128)。")
|
||
# 验证精度设置
|
||
if MATRIX_CACHE:
|
||
sample_key = list(MATRIX_CACHE.keys())[0]
|
||
sample_data = MATRIX_CACHE[sample_key]
|
||
print(f"示例矩阵精度检查: values.dtype={sample_data['values'].dtype}, b.dtype={sample_data['b'].dtype}")
|
||
|
||
def get_Ab(k_idx, device, dtype=None):
|
||
"""
|
||
从缓存中获取当前设备对应的 A 和 b。
|
||
如果矩阵在 CPU 上,则动态传输到目标设备(使用 non_blocking 加速)。
|
||
|
||
Args:
|
||
k_idx: 样本索引
|
||
device: 目标设备
|
||
dtype: 目标数据类型,如果为None则保持原有精度
|
||
|
||
优化:
|
||
1. 矩阵存储在 CPU,需要时才传输到 GPU
|
||
2. 使用 non_blocking=True 进行异步传输
|
||
3. 使用 pin_memory 加速传输
|
||
"""
|
||
if k_idx not in MATRIX_CACHE:
|
||
raise RuntimeError(f"未找到样本 k={k_idx} 的缓存数据")
|
||
|
||
cache_data = MATRIX_CACHE[k_idx]
|
||
|
||
# 如果目标设备是 CPU,在 CPU 上构建稀疏矩阵
|
||
if device.type == 'cpu':
|
||
indices = cache_data['indices']
|
||
values = cache_data['values']
|
||
shape = cache_data['shape']
|
||
b_cpu = cache_data['b']
|
||
|
||
# 根据需要转换数据类型
|
||
if dtype is not None:
|
||
values = values.to(dtype)
|
||
b_cpu = b_cpu.to(dtype)
|
||
|
||
A_cpu = torch.sparse_coo_tensor(indices, values, shape, device=torch.device('cpu'))
|
||
return A_cpu, b_cpu
|
||
|
||
# 如果目标设备是 GPU,将矩阵传输到 GPU
|
||
# 使用 non_blocking=True 进行异步传输(需要 pin_memory)
|
||
try:
|
||
# 获取已 pin_memory 的 indices 和 values
|
||
indices_cpu = cache_data['indices']
|
||
values_cpu = cache_data['values']
|
||
shape = cache_data['shape']
|
||
b_cpu = cache_data['b']
|
||
|
||
# 根据需要转换数据类型
|
||
if dtype is not None:
|
||
values_cpu = values_cpu.to(dtype)
|
||
b_cpu = b_cpu.to(dtype)
|
||
|
||
# 异步传输到 GPU(non_blocking 需要 pin_memory)
|
||
indices_gpu = indices_cpu.to(device, non_blocking=True)
|
||
values_gpu = values_cpu.to(device, non_blocking=True)
|
||
|
||
# 在 GPU 上重建稀疏矩阵并 coalesce
|
||
A_gpu = torch.sparse_coo_tensor(indices_gpu, values_gpu, shape, device=device).coalesce()
|
||
|
||
# 异步传输 b 向量
|
||
b_gpu = b_cpu.to(device, non_blocking=True)
|
||
|
||
return A_gpu, b_gpu
|
||
except Exception as e:
|
||
# 如果异步传输失败,回退到同步传输
|
||
if is_main_process():
|
||
print(f"警告: 异步传输失败,使用同步传输 (k={k_idx}): {e}")
|
||
indices_cpu = cache_data['indices']
|
||
values_cpu = cache_data['values']
|
||
shape = cache_data['shape']
|
||
b_cpu = cache_data['b']
|
||
|
||
# 根据需要转换数据类型
|
||
if dtype is not None:
|
||
values_cpu = values_cpu.to(dtype)
|
||
b_cpu = b_cpu.to(dtype)
|
||
|
||
indices_gpu = indices_cpu.to(device)
|
||
values_gpu = values_cpu.to(device)
|
||
A_gpu = torch.sparse_coo_tensor(indices_gpu, values_gpu, shape, device=device).coalesce()
|
||
b_gpu = b_cpu.to(device)
|
||
|
||
return A_gpu, b_gpu
|
||
|
||
# ==========================================
|
||
# 3. 辅助函数:绘制训练曲线
|
||
# ==========================================
|
||
def plot_training_curve(train_losses=None, test_losses=None, save_path="training_curve.svg", data_file=None):
|
||
"""
|
||
绘制训练集和测试集loss变化曲线并保存为文件
|
||
|
||
Args:
|
||
train_losses: 训练集loss列表(可选,如果提供data_file则忽略)
|
||
test_losses: 测试集loss列表(可选,如果提供data_file则忽略)
|
||
save_path: 保存路径
|
||
data_file: 训练数据JSON文件路径,如果提供则从文件加载数据
|
||
"""
|
||
# 在DDP环境中,只在主进程中执行绘图
|
||
if not is_main_process():
|
||
return
|
||
if data_file is not None:
|
||
# 从文件加载数据
|
||
data = load_training_data(data_file)
|
||
if data is None:
|
||
if is_main_process():
|
||
print("❌ 无法加载训练数据,跳过绘图")
|
||
return
|
||
train_losses = data.get('train_losses', [])
|
||
test_losses = data.get('test_losses', [])
|
||
if not train_losses or not test_losses:
|
||
if is_main_process():
|
||
print("❌ 训练数据中缺少loss信息,跳过绘图")
|
||
return
|
||
plt.figure(figsize=(12, 6))
|
||
|
||
epochs = range(1, len(train_losses) + 1)
|
||
|
||
# 绘制训练loss
|
||
plt.subplot(1, 2, 1)
|
||
plt.plot(epochs, train_losses, 'b-', label='Training Loss', linewidth=2)
|
||
plt.xlabel('Epoch')
|
||
plt.ylabel('Loss')
|
||
plt.title('Training Loss Curve')
|
||
plt.legend()
|
||
plt.grid(True, alpha=0.3)
|
||
plt.yscale('log') # 使用对数尺度
|
||
|
||
# 绘制测试loss
|
||
plt.subplot(1, 2, 2)
|
||
plt.plot(epochs, test_losses, 'r-', label='Test Loss', linewidth=2)
|
||
plt.xlabel('Epoch')
|
||
plt.ylabel('Loss')
|
||
plt.title('Test Loss Curve')
|
||
plt.legend()
|
||
plt.grid(True, alpha=0.3)
|
||
plt.yscale('log') # 使用对数尺度
|
||
|
||
# 保存为矢量图格式
|
||
# PDF格式(高质量打印)
|
||
pdf_path = os.path.join(os.path.dirname(save_path), os.path.basename(save_path).replace('.png', '.pdf'))
|
||
plt.savefig(pdf_path, bbox_inches='tight')
|
||
plt.close()
|
||
|
||
if is_main_process():
|
||
print(f"✅ 训练曲线PDF已保存到: {pdf_path}")
|
||
|
||
# SVG格式(网页和现代应用)
|
||
plt.figure(figsize=(12, 6))
|
||
|
||
# 重新绘制(合并在一个图中)
|
||
plt.plot(epochs, train_losses, 'b-', label='Training Loss', linewidth=2)
|
||
plt.plot(epochs, test_losses, 'r-', label='Test Loss', linewidth=2)
|
||
plt.xlabel('Epoch')
|
||
plt.ylabel('Loss')
|
||
plt.title('Training and Test Loss Curves')
|
||
plt.legend()
|
||
plt.grid(True, alpha=0.3)
|
||
plt.yscale('log') # 使用对数尺度
|
||
|
||
svg_path = os.path.join(os.path.dirname(save_path), os.path.basename(save_path).replace('.png', '.svg'))
|
||
plt.savefig(svg_path, bbox_inches='tight')
|
||
plt.close()
|
||
|
||
if is_main_process():
|
||
print(f"✅ 训练曲线SVG已保存到: {svg_path}")
|
||
|
||
|
||
def plot_mse_res_loss(train_mse=None, test_mse=None, train_res=None, test_res=None, save_path="mse_res_loss.svg", data_file=None):
|
||
"""
|
||
绘制包含MSE loss和RES loss的训练曲线
|
||
|
||
Args:
|
||
train_mse: 训练集MSE loss列表(可选,如果提供data_file则忽略)
|
||
test_mse: 测试集MSE loss列表(可选,如果提供data_file则忽略)
|
||
train_res: 训练集RES loss列表(可选,如果提供data_file则忽略)
|
||
test_res: 测试集RES loss列表(可选,如果提供data_file则忽略)
|
||
save_path: 保存路径
|
||
data_file: 训练数据JSON文件路径,如果提供则从文件加载数据
|
||
"""
|
||
# 在DDP环境中,只在主进程中执行绘图
|
||
if not is_main_process():
|
||
return
|
||
if data_file is not None:
|
||
# 从文件加载数据
|
||
data = load_training_data(data_file)
|
||
if data is None:
|
||
if is_main_process():
|
||
print("❌ 无法加载训练数据,跳过绘图")
|
||
return
|
||
train_mse = data.get('train_mse_losses', [])
|
||
test_mse = data.get('test_mse_losses', [])
|
||
train_res = data.get('train_res_losses', [])
|
||
test_res = data.get('test_res_losses', [])
|
||
if not all([train_mse, test_mse, train_res, test_res]):
|
||
if is_main_process():
|
||
print("❌ 训练数据中缺少MSE或RES loss信息,跳过绘图")
|
||
return
|
||
plt.figure(figsize=(14, 6))
|
||
|
||
epochs = range(1, len(train_mse) + 1)
|
||
|
||
# 绘制MSE loss
|
||
plt.subplot(1, 2, 1)
|
||
plt.plot(epochs, train_mse, 'b-', label='Training MSE', linewidth=2, marker='o', markersize=3)
|
||
plt.plot(epochs, test_mse, 'r-', label='Testing MSE', linewidth=2, marker='+', markersize=3)
|
||
plt.xlabel('Epoch')
|
||
plt.ylabel('MSE Loss (log scale)')
|
||
plt.title('MSE Loss Curve')
|
||
plt.legend()
|
||
plt.grid(True, alpha=0.3)
|
||
plt.yscale('log')
|
||
|
||
# 绘制RES loss
|
||
plt.subplot(1, 2, 2)
|
||
plt.plot(epochs, train_res, 'b-', label='Training Res', linewidth=2, marker='o', markersize=3)
|
||
plt.plot(epochs, test_res, 'r-', label='Testing Res', linewidth=2, marker='+', markersize=3)
|
||
plt.xlabel('Epoch')
|
||
plt.ylabel('RES loss')
|
||
plt.title('Residual Loss Curve')
|
||
plt.legend()
|
||
plt.grid(True, alpha=0.3)
|
||
plt.ylim(0, 0.2) # 设置y轴范围为0~0.2
|
||
|
||
# 保存为矢量图格式 (SVG)
|
||
svg_path = os.path.join(os.path.dirname(save_path), os.path.basename(save_path).replace('.png', '.svg'))
|
||
plt.savefig(svg_path, bbox_inches='tight')
|
||
plt.close()
|
||
|
||
if is_main_process():
|
||
print(f"✅ MSE和RES loss曲线SVG已保存到: {svg_path}")
|
||
|
||
|
||
def plot_mse_loss_distribution(train_mse_losses=None, test_mse_losses=None, save_path="mse_loss_distribution.svg", data_file=None):
|
||
"""
|
||
绘制MSE loss分布柱状图
|
||
|
||
Args:
|
||
train_mse_losses: 训练集MSE loss列表(可选,如果提供data_file则忽略)
|
||
test_mse_losses: 测试集MSE loss列表(可选,如果提供data_file则忽略)
|
||
save_path: 保存路径
|
||
data_file: 训练数据JSON文件路径,如果提供则从文件加载数据
|
||
"""
|
||
# 在DDP环境中,只在主进程中执行绘图
|
||
if not is_main_process():
|
||
return
|
||
if data_file is not None:
|
||
# 从文件加载数据
|
||
data = load_training_data(data_file)
|
||
if data is None:
|
||
if is_main_process():
|
||
print("❌ 无法加载训练数据,跳过绘图")
|
||
return
|
||
train_mse_losses = data.get('train_mse_losses', [])
|
||
test_mse_losses = data.get('test_mse_losses', [])
|
||
if not train_mse_losses or not test_mse_losses:
|
||
if is_main_process():
|
||
print("❌ 训练数据中缺少MSE loss信息,跳过绘图")
|
||
return
|
||
plt.figure(figsize=(12, 6))
|
||
|
||
# 确保输入是有效的数值列表
|
||
if not train_mse_losses or not test_mse_losses:
|
||
if is_main_process():
|
||
print("⚠️ 警告:训练集或测试集MSE loss数据为空,跳过绘图")
|
||
plt.close()
|
||
return
|
||
|
||
# 转换为numpy数组并过滤无效值
|
||
train_mse_losses = np.array(train_mse_losses, dtype=np.float32)
|
||
test_mse_losses = np.array(test_mse_losses, dtype=np.float32)
|
||
|
||
# 过滤掉NaN和inf值
|
||
train_mse_losses = train_mse_losses[np.isfinite(train_mse_losses)]
|
||
test_mse_losses = test_mse_losses[np.isfinite(test_mse_losses)]
|
||
|
||
if len(train_mse_losses) == 0 or len(test_mse_losses) == 0:
|
||
if is_main_process():
|
||
print("⚠️ 警告:过滤后训练集或测试集MSE loss数据为空,跳过绘图")
|
||
plt.close()
|
||
return
|
||
|
||
# 计算统计信息
|
||
train_mean = np.mean(train_mse_losses)
|
||
train_std = np.std(train_mse_losses)
|
||
test_mean = np.mean(test_mse_losses)
|
||
test_std = np.std(test_mse_losses)
|
||
|
||
# 设置bins(对数坐标需要特殊的处理)
|
||
all_mse_losses = np.concatenate([train_mse_losses, test_mse_losses])
|
||
# 确保所有值都是正数(MSE loss应该都是正数)
|
||
all_mse_losses = all_mse_losses[all_mse_losses > 0]
|
||
if len(all_mse_losses) == 0:
|
||
if is_main_process():
|
||
print("⚠️ 警告:所有MSE loss值都是非正数,跳过绘图")
|
||
plt.close()
|
||
return
|
||
|
||
# 创建对数bins
|
||
log_min = np.log10(max(all_mse_losses.min(), 1e-10)) # 避免log(0)
|
||
log_max = np.log10(all_mse_losses.max())
|
||
bins = np.logspace(log_min, log_max, 50)
|
||
|
||
# 计算直方图数据
|
||
train_hist, _ = np.histogram(train_mse_losses, bins=bins, density=True)
|
||
test_hist, _ = np.histogram(test_mse_losses, bins=bins, density=True)
|
||
|
||
# 计算bin中心用于绘制
|
||
bin_centers = (bins[:-1] + bins[1:]) / 2
|
||
|
||
# 设置bar宽度
|
||
bar_width = np.diff(bins) * 0.8 # 每个bin的80%宽度
|
||
|
||
# 绘制训练集和测试集的柱状图(分开放置)
|
||
plt.bar(bin_centers - bar_width/4, train_hist, width=bar_width/2, alpha=0.8, color='coral',
|
||
label=f'Training (μ={train_mean:.3f}, σ={train_std:.5f})', edgecolor='black', linewidth=0.5)
|
||
|
||
plt.bar(bin_centers + bar_width/4, test_hist, width=bar_width/2, alpha=0.8, color='cyan',
|
||
label=f'Testing (μ={test_mean:.3f}, σ={test_std:.5f})', edgecolor='black', linewidth=0.5)
|
||
|
||
# 拟合高斯分布并绘制
|
||
from scipy import stats
|
||
|
||
# 训练集高斯拟合
|
||
train_params = stats.norm.fit(train_mse_losses)
|
||
train_x = np.logspace(log_min, log_max, 100) # 在对数空间均匀分布的点用于显示
|
||
train_pdf = stats.norm.pdf(train_x, *train_params)
|
||
plt.plot(train_x, train_pdf, 'r-', linewidth=2, label='Training Gaussian Fit')
|
||
|
||
# 测试集高斯拟合
|
||
test_params = stats.norm.fit(test_mse_losses)
|
||
test_x = np.logspace(log_min, log_max, 100) # 在对数空间均匀分布的点用于显示
|
||
test_pdf = stats.norm.pdf(test_x, *test_params)
|
||
plt.plot(test_x, test_pdf, 'b-', linewidth=2, label='Testing Gaussian Fit')
|
||
|
||
plt.xlabel('MSE Loss (log scale)')
|
||
plt.ylabel('Density')
|
||
plt.title('MSE Loss Distribution')
|
||
plt.legend()
|
||
plt.grid(True, alpha=0.3)
|
||
plt.xscale('log') # 设置x轴为对数坐标
|
||
|
||
# 保存为矢量图格式 (SVG)
|
||
svg_path = os.path.join(os.path.dirname(save_path), os.path.basename(save_path).replace('.png', '.svg'))
|
||
plt.savefig(svg_path, bbox_inches='tight')
|
||
plt.close()
|
||
|
||
if is_main_process():
|
||
print(f"✅ MSE loss分布图SVG已保存到: {svg_path}")
|
||
print(f" 训练集: 均值={train_mean:.6f}, 标准差={train_std:.6f}")
|
||
print(f" 测试集: 均值={test_mean:.6f}, 标准差={test_std:.6f}")
|
||
|
||
|
||
def compute_relative_errors(solver, data_loader, data_mapping, device, matrix_dtype):
|
||
"""
|
||
计算数据集中每个样本的相对误差
|
||
|
||
Args:
|
||
solver: 训练好的模型
|
||
data_loader: 数据加载器
|
||
data_mapping: 数据映射字典
|
||
device: 设备
|
||
matrix_dtype: 矩阵数据类型
|
||
|
||
Returns:
|
||
relative_errors: 每个样本的相对误差列表
|
||
"""
|
||
solver.eval()
|
||
relative_errors = []
|
||
|
||
with torch.no_grad():
|
||
for data_list in data_loader:
|
||
# 获取batch
|
||
if not isinstance(data_list, list):
|
||
data_list = [data_list]
|
||
|
||
processed_data_list = []
|
||
for item in data_list:
|
||
if isinstance(item, Data):
|
||
if not hasattr(item, 'k_idx'):
|
||
item.k_idx = torch.tensor([0])
|
||
elif not isinstance(item.k_idx, torch.Tensor):
|
||
item.k_idx = torch.tensor([item.k_idx] if not isinstance(item.k_idx, (list, tuple)) else item.k_idx)
|
||
processed_data_list.append(item)
|
||
elif isinstance(item, tuple) and len(item) > 0 and isinstance(item[0], Data):
|
||
processed_data_list.append(item[0])
|
||
|
||
try:
|
||
batch = Batch.from_data_list(processed_data_list)
|
||
except:
|
||
from torch_geometric.data.collate import collate
|
||
batch, _, _ = collate(Data, processed_data_list, increment=True, add_batch=True, follow_batch=[])
|
||
|
||
batch = batch.to(device)
|
||
k_all = batch.k_idx
|
||
node_batch = batch.batch
|
||
B = k_all.size(0)
|
||
|
||
# 获取模型精度 - 兼容不同层类型
|
||
raw_model = solver.module if isinstance(solver, (DataParallel, DDP)) else solver
|
||
if hasattr(raw_model.model_real.networks[0].gcn1, 'conv'):
|
||
is_double_precision = raw_model.model_real.networks[0].gcn1.conv.lin_fusion.weight.dtype == torch.float64
|
||
elif hasattr(raw_model.model_real.networks[0].gcn1, 'spatial_conv'):
|
||
is_double_precision = raw_model.model_real.networks[0].gcn1.spatial_conv.lin_fusion.weight.dtype == torch.float64
|
||
elif hasattr(raw_model.model_real.networks[0].gcn1, 'linear'):
|
||
is_double_precision = raw_model.model_real.networks[0].gcn1.linear.weight.dtype == torch.float64
|
||
else:
|
||
is_double_precision = False
|
||
|
||
if is_double_precision:
|
||
data_dtype = torch.float64
|
||
else:
|
||
data_dtype = torch.float32
|
||
|
||
# 准备数据
|
||
eps_feat = batch.x[:, 0:2].to(data_dtype)
|
||
current_E_real = batch.x[:, 4].to(data_dtype)
|
||
current_E_imag = batch.x[:, 5].to(data_dtype)
|
||
true_real = batch.y[:, 0].to(data_dtype)
|
||
true_imag = batch.y[:, 1].to(data_dtype)
|
||
|
||
# 加载矩阵
|
||
A_list = []
|
||
b_list = []
|
||
for b_idx in range(B):
|
||
k = int(k_all[b_idx].item())
|
||
A, b = get_Ab(k, device, matrix_dtype)
|
||
A_list.append(A)
|
||
b_list.append(b)
|
||
|
||
# 前向传播
|
||
E_real_cur = current_E_real
|
||
E_imag_cur = current_E_imag
|
||
|
||
for iter_idx in range(raw_model.n_iter):
|
||
# 计算残差
|
||
r_real_list = []
|
||
r_imag_list = []
|
||
for b_idx in range(B):
|
||
mask = (node_batch == b_idx)
|
||
E_r = E_real_cur[mask]
|
||
E_i = E_imag_cur[mask]
|
||
A = A_list[b_idx]
|
||
b_vec = b_list[b_idx]
|
||
E_c = torch.complex(E_r, E_i)
|
||
Ax = torch.sparse.mm(A, E_c.unsqueeze(-1)).squeeze(-1)
|
||
r_c = b_vec - Ax
|
||
if data_dtype == torch.float64:
|
||
r_real_list.append(r_c.real.double())
|
||
r_imag_list.append(r_c.imag.double())
|
||
else:
|
||
r_real_list.append(r_c.real.float())
|
||
r_imag_list.append(r_c.imag.float())
|
||
del E_c, Ax, r_c
|
||
|
||
r_real = torch.cat(r_real_list, dim=0)
|
||
r_imag = torch.cat(r_imag_list, dim=0)
|
||
del r_real_list, r_imag_list
|
||
|
||
# GNN前向
|
||
# 从batch.x中提取背景场
|
||
bg_real = batch.x[:, 6].to(data_dtype)
|
||
bg_imag = batch.x[:, 7].to(data_dtype)
|
||
x_in = torch.cat([
|
||
eps_feat,
|
||
r_real.view(-1, 1),
|
||
r_imag.view(-1, 1),
|
||
E_real_cur.view(-1, 1), # 当前电场实部 (随迭代更新)
|
||
E_imag_cur.view(-1, 1), # 当前电场虚部 (随迭代更新)
|
||
bg_real.view(-1, 1), # 背景场实部 (不随网络更新)
|
||
bg_imag.view(-1, 1) # 背景场虚部 (不随网络更新)
|
||
], dim=1)
|
||
|
||
delta_real = raw_model.model_real(x_in, batch.edge_index, batch.batch, iter_idx)
|
||
delta_imag = raw_model.model_imag(x_in, batch.edge_index, batch.batch, iter_idx)
|
||
|
||
E_real_cur = E_real_cur + delta_real.view(-1)
|
||
E_imag_cur = E_imag_cur + delta_imag.view(-1)
|
||
del x_in, delta_real, delta_imag, r_real, r_imag
|
||
|
||
# 计算每个样本的相对误差
|
||
for b_idx in range(B):
|
||
mask = (node_batch == b_idx)
|
||
pred_real = E_real_cur[mask]
|
||
pred_imag = E_imag_cur[mask]
|
||
true_r = true_real[mask]
|
||
true_i = true_imag[mask]
|
||
|
||
# 计算平方的相对误差: ||pred - true||_2^2 / ||true||_2^2(与公式一致)
|
||
# 分子:||pred - true||_2^2
|
||
numerator = ((pred_real - true_r).pow(2).sum() + (pred_imag - true_i).pow(2).sum())
|
||
# 分母:||true||_2^2
|
||
denominator = (true_r.pow(2).sum() + true_i.pow(2).sum())
|
||
|
||
if denominator > 1e-10: # 避免除零
|
||
# 平方的相对误差:||x^FEM - x^GCN||_2^2 / ||x^FEM||_2^2
|
||
rel_error_squared = (numerator / denominator).item()
|
||
relative_errors.append(rel_error_squared)
|
||
|
||
del A_list, b_list, E_real_cur, E_imag_cur
|
||
|
||
return relative_errors
|
||
|
||
def save_training_data(training_data, save_path="training_data.json"):
|
||
"""
|
||
保存训练数据到JSON文件
|
||
|
||
Args:
|
||
training_data: 包含训练数据的字典
|
||
save_path: 保存路径
|
||
"""
|
||
# 在DDP环境中,只在主进程中保存数据
|
||
if not is_main_process():
|
||
return
|
||
import json
|
||
|
||
# 将numpy类型转换为Python原生类型,以便JSON序列化
|
||
def convert_to_serializable(obj):
|
||
if isinstance(obj, dict):
|
||
return {k: convert_to_serializable(v) for k, v in obj.items()}
|
||
elif isinstance(obj, list):
|
||
return [convert_to_serializable(item) for item in obj]
|
||
elif isinstance(obj, (np.integer, np.floating)):
|
||
return obj.item()
|
||
elif isinstance(obj, (int, float, str, bool)) or obj is None:
|
||
return obj
|
||
else:
|
||
return str(obj)
|
||
|
||
serializable_data = convert_to_serializable(training_data)
|
||
|
||
with open(save_path, 'w') as f:
|
||
json.dump(serializable_data, f, indent=2)
|
||
|
||
if is_main_process():
|
||
print(f"✅ 训练数据已保存到: {save_path}")
|
||
|
||
def load_training_data(load_path="training_data.json"):
|
||
"""
|
||
从JSON文件加载训练数据
|
||
|
||
Args:
|
||
load_path: 加载路径
|
||
|
||
Returns:
|
||
dict: 训练数据字典
|
||
"""
|
||
import json
|
||
|
||
try:
|
||
with open(load_path, 'r') as f:
|
||
data = json.load(f)
|
||
if is_main_process():
|
||
print(f"✅ 训练数据已从 {load_path} 加载")
|
||
return data
|
||
except FileNotFoundError:
|
||
if is_main_process():
|
||
print(f"⚠️ 警告:找不到训练数据文件 {load_path}")
|
||
return None
|
||
except Exception as e:
|
||
if is_main_process():
|
||
print(f"❌ 加载训练数据失败: {e}")
|
||
return None
|
||
|
||
# ==========================================
|
||
# 3. 辅助函数:处理 DataParallel 的输出
|
||
# ==========================================
|
||
def extract_loss_and_num_nodes(outputs):
|
||
"""
|
||
从 DataParallel 的输出中提取 loss 和 num_nodes。
|
||
|
||
注意:forward 方法返回单个 tensor:
|
||
- 普通模式:[loss_sum, res_sum, num_nodes, num_samples] (4个值)
|
||
- Hybrid模式:[loss_sum, res_sum, num_nodes, num_samples, mse_loss_sum, phi_loss_sum] (6个值)
|
||
|
||
Args:
|
||
outputs: DataParallel 返回的结果
|
||
- 如果是列表,每个元素是上述格式的 tensor
|
||
- 如果是单个 tensor,形状为 [4]/[6] 或 [N, 4]/[N, 6](N 个 GPU)
|
||
|
||
Returns:
|
||
tuple: (total_loss_sum, total_res_sum, total_num_nodes, total_num_samples, total_mse_sum, total_phi_sum)
|
||
- total_loss_sum: 所有 GPU 的 loss 总和 (tensor)
|
||
- total_res_sum: 所有 GPU 的 RES loss 总和 (tensor)
|
||
- total_num_nodes: 所有 GPU 的 num_nodes 总和 (int)
|
||
- total_num_samples: 所有 GPU 的 num_samples 总和 (int)
|
||
- total_mse_sum: 所有 GPU 的 MSE loss 总和 (tensor,仅在hybrid模式下有效)
|
||
- total_phi_sum: 所有 GPU 的 Phi loss 总和 (tensor,仅在hybrid模式下有效)
|
||
"""
|
||
# 检查 outputs 是否为空(只检查 None 和空列表,不检查张量)
|
||
if outputs is None:
|
||
return torch.tensor(0.0), torch.tensor(0.0), 0, 0, torch.tensor(0.0), torch.tensor(0.0)
|
||
if isinstance(outputs, list) and len(outputs) == 0:
|
||
return torch.tensor(0.0), torch.tensor(0.0), 0, 0, torch.tensor(0.0), torch.tensor(0.0)
|
||
|
||
# 处理 DataParallel 的输出格式
|
||
# DataParallel 可能返回:
|
||
# 1. 列表: [[loss1, res1, n1, s1, mse1, phi1], [loss2, res2, n2, s2, mse2, phi2], ...] (hybrid模式)
|
||
# 2. 列表: [[loss1, res1, n1, s1], [loss2, res2, n2, s2], ...] (普通模式)
|
||
# 3. 单个 tensor: [[loss1, res1, n1, s1, mse1, phi1], ...] 形状为 [N, 6] 或 [N, 4]
|
||
# 4. 单个 tensor: [loss, res, n, s, mse, phi] 形状为 [6] 或 [4](单 GPU 情况)
|
||
|
||
if isinstance(outputs, list):
|
||
# 列表格式:每个元素是 [loss, res, num_nodes, num_samples, mse?, phi?] 的 tensor
|
||
loss_list = []
|
||
res_list = []
|
||
num_nodes_list = []
|
||
num_samples_list = []
|
||
mse_list = []
|
||
phi_list = []
|
||
is_hybrid_mode = False
|
||
|
||
for o in outputs:
|
||
if isinstance(o, torch.Tensor):
|
||
if o.dim() == 1:
|
||
if o.shape[0] == 4:
|
||
# 普通模式:形状为 [4] 的 tensor
|
||
loss_list.append(o[0])
|
||
res_list.append(o[1])
|
||
num_nodes_list.append(o[2].item())
|
||
num_samples_list.append(o[3].item())
|
||
mse_list.append(torch.tensor(0.0)) # 占位符
|
||
phi_list.append(torch.tensor(0.0)) # 占位符
|
||
elif o.shape[0] == 6:
|
||
# Hybrid模式:形状为 [6] 的 tensor
|
||
is_hybrid_mode = True
|
||
loss_list.append(o[0])
|
||
res_list.append(o[1])
|
||
num_nodes_list.append(o[2].item())
|
||
num_samples_list.append(o[3].item())
|
||
mse_list.append(o[4])
|
||
phi_list.append(o[5])
|
||
else:
|
||
raise ValueError(f"不支持的 tensor 形状: {o.shape}")
|
||
elif o.dim() == 2:
|
||
if o.shape[1] == 4:
|
||
# 普通模式:形状为 [N, 4] 的 tensor(多个 GPU 合并)
|
||
loss_list.append(o[:, 0].sum())
|
||
res_list.append(o[:, 1].sum())
|
||
num_nodes_list.append(o[:, 2].sum().item())
|
||
num_samples_list.append(o[:, 3].sum().item())
|
||
mse_list.append(torch.tensor(0.0)) # 占位符
|
||
phi_list.append(torch.tensor(0.0)) # 占位符
|
||
elif o.shape[1] == 6:
|
||
# Hybrid模式:形状为 [N, 6] 的 tensor(多个 GPU 合并)
|
||
is_hybrid_mode = True
|
||
loss_list.append(o[:, 0].sum())
|
||
res_list.append(o[:, 1].sum())
|
||
num_nodes_list.append(o[:, 2].sum().item())
|
||
num_samples_list.append(o[:, 3].sum().item())
|
||
mse_list.append(o[:, 4].sum())
|
||
phi_list.append(o[:, 5].sum())
|
||
else:
|
||
raise ValueError(f"不支持的 tensor 形状: {o.shape}")
|
||
else:
|
||
raise ValueError(f"不支持的 tensor 形状: {o.shape}")
|
||
else:
|
||
raise TypeError(f"不支持的输出类型: {type(o)}")
|
||
|
||
total_loss = torch.stack(loss_list).sum() if loss_list else torch.tensor(0.0)
|
||
total_res = torch.stack(res_list).sum() if res_list else torch.tensor(0.0)
|
||
total_num_nodes = sum(num_nodes_list)
|
||
total_num_samples = sum(num_samples_list)
|
||
total_mse = torch.stack(mse_list).sum() if is_hybrid_mode and mse_list else torch.tensor(0.0)
|
||
total_phi = torch.stack(phi_list).sum() if is_hybrid_mode and phi_list else torch.tensor(0.0)
|
||
|
||
elif isinstance(outputs, torch.Tensor):
|
||
# 单个 tensor 格式
|
||
if outputs.dim() == 1:
|
||
if outputs.shape[0] == 4:
|
||
# 普通模式:形状为 [4]:单 GPU
|
||
total_loss = outputs[0]
|
||
total_res = outputs[1]
|
||
total_num_nodes = int(outputs[2].item())
|
||
total_num_samples = int(outputs[3].item())
|
||
total_mse = torch.tensor(0.0)
|
||
total_phi = torch.tensor(0.0)
|
||
elif outputs.shape[0] == 6:
|
||
# Hybrid模式:形状为 [6]:单 GPU
|
||
total_loss = outputs[0]
|
||
total_res = outputs[1]
|
||
total_num_nodes = int(outputs[2].item())
|
||
total_num_samples = int(outputs[3].item())
|
||
total_mse = outputs[4]
|
||
total_phi = outputs[5]
|
||
elif outputs.shape[0] % 4 == 0 and outputs.shape[0] % 6 != 0:
|
||
# 普通模式:形状为 [4*N]:多个 GPU 的输出被展平(例如 [12] = 3个GPU * 4)
|
||
# 需要重塑为 [N, 4] 格式
|
||
n_gpus = outputs.shape[0] // 4
|
||
outputs_reshaped = outputs.view(n_gpus, 4)
|
||
total_loss = outputs_reshaped[:, 0].sum()
|
||
total_res = outputs_reshaped[:, 1].sum()
|
||
total_num_nodes = int(outputs_reshaped[:, 2].sum().item())
|
||
total_num_samples = int(outputs_reshaped[:, 3].sum().item())
|
||
total_mse = torch.tensor(0.0)
|
||
total_phi = torch.tensor(0.0)
|
||
elif outputs.shape[0] % 6 == 0:
|
||
# Hybrid模式:形状为 [6*N]:多个 GPU 的输出被展平
|
||
# 需要重塑为 [N, 6] 格式
|
||
n_gpus = outputs.shape[0] // 6
|
||
outputs_reshaped = outputs.view(n_gpus, 6)
|
||
total_loss = outputs_reshaped[:, 0].sum()
|
||
total_res = outputs_reshaped[:, 1].sum()
|
||
total_num_nodes = int(outputs_reshaped[:, 2].sum().item())
|
||
total_num_samples = int(outputs_reshaped[:, 3].sum().item())
|
||
total_mse = outputs_reshaped[:, 4].sum()
|
||
total_phi = outputs_reshaped[:, 5].sum()
|
||
else:
|
||
raise ValueError(f"不支持的 tensor 形状: {outputs.shape}(一维张量长度必须是4或6的倍数)")
|
||
elif outputs.dim() == 2:
|
||
if outputs.shape[1] == 4:
|
||
# 普通模式:形状为 [N, 4]:多个 GPU 合并
|
||
total_loss = outputs[:, 0].sum()
|
||
total_res = outputs[:, 1].sum()
|
||
total_num_nodes = int(outputs[:, 2].sum().item())
|
||
total_num_samples = int(outputs[:, 3].sum().item())
|
||
total_mse = torch.tensor(0.0)
|
||
total_phi = torch.tensor(0.0)
|
||
elif outputs.shape[1] == 6:
|
||
# Hybrid模式:形状为 [N, 6]:多个 GPU 合并
|
||
total_loss = outputs[:, 0].sum()
|
||
total_res = outputs[:, 1].sum()
|
||
total_num_nodes = int(outputs[:, 2].sum().item())
|
||
total_num_samples = int(outputs[:, 3].sum().item())
|
||
total_mse = outputs[:, 4].sum()
|
||
total_phi = outputs[:, 5].sum()
|
||
else:
|
||
raise ValueError(f"不支持的 tensor 形状: {outputs.shape}")
|
||
else:
|
||
raise ValueError(f"不支持的 tensor 形状: {outputs.shape}")
|
||
else:
|
||
raise TypeError(f"不支持的输出类型: {type(outputs)}")
|
||
|
||
return total_loss, total_res, total_num_nodes, total_num_samples, total_mse, total_phi
|
||
|
||
|
||
# ==========================================
|
||
# 4. Loss计算函数
|
||
# ==========================================
|
||
def compute_mse_loss(E_real_cur, E_imag_cur, batch_y, num_nodes=None):
|
||
"""
|
||
计算传统的MSE损失:||pred - true||^2(返回总和,未平均)
|
||
|
||
Args:
|
||
E_real_cur: 预测的实部电场 [N]
|
||
E_imag_cur: 预测的虚部电场 [N]
|
||
batch_y: 真实标签 [N, 2]
|
||
num_nodes: 节点总数(用于接口统一,不使用)
|
||
|
||
Returns:
|
||
loss_sum: MSE损失总和(未平均)
|
||
"""
|
||
true_real = batch_y[:, 0]
|
||
true_imag = batch_y[:, 1]
|
||
|
||
# 计算实部和虚部的平方误差总和
|
||
term1 = (E_real_cur - true_real).pow(2).sum() # 实部误差平方和
|
||
term2 = (E_imag_cur - true_imag).pow(2).sum() # 虚部误差平方和
|
||
|
||
# 返回总SSE(Sum of Squared Errors)
|
||
loss_sum = 0.5 * (term1 + term2)
|
||
|
||
# 释放中间变量
|
||
del term1, term2
|
||
|
||
return loss_sum
|
||
|
||
def compute_phi_loss(E_real_cur, E_imag_cur, k_all, node_batch, B, device, matrix_dtype, num_nodes=None):
|
||
"""
|
||
计算Phi损失:||A*x - b||^2(物理残差,不需要节点平均)
|
||
|
||
Args:
|
||
E_real_cur: 预测的实部电场 [N]
|
||
E_imag_cur: 预测的虚部电场 [N]
|
||
k_all: 样本索引 [B]
|
||
node_batch: 节点到批次的映射 [N]
|
||
B: 批次大小
|
||
device: 计算设备
|
||
matrix_dtype: 矩阵数据类型
|
||
num_nodes: 节点总数(Phi loss不使用,用于接口统一)
|
||
|
||
Returns:
|
||
loss_sum: Phi损失总和(不进行节点平均)
|
||
"""
|
||
phi_losses = []
|
||
for b_idx in range(B):
|
||
k = int(k_all[b_idx].item())
|
||
A, b = get_Ab(k, device, matrix_dtype)
|
||
|
||
mask = (node_batch == b_idx)
|
||
E_r = E_real_cur[mask]
|
||
E_i = E_imag_cur[mask]
|
||
|
||
E_c = torch.complex(E_r, E_i)
|
||
Ax = torch.sparse.mm(A, E_c.unsqueeze(-1)).squeeze(-1)
|
||
r = b - Ax
|
||
|
||
# 计算残差的L2范数平方: ||Ax - b||_2^2
|
||
phi_loss = torch.norm(r, p=2).pow(2)
|
||
phi_losses.append(phi_loss)
|
||
|
||
# 使用torch.stack和torch.sum保持梯度计算图
|
||
loss_sum = torch.stack(phi_losses).sum()
|
||
|
||
return loss_sum
|
||
|
||
def compute_asinh_loss(E_real_cur, E_imag_cur, batch_y, num_nodes):
|
||
"""
|
||
计算Asinh损失:sqrt(asinh(||pred - true||^2))(不需要节点平均)
|
||
|
||
Args:
|
||
E_real_cur: 预测的实部电场 [N]
|
||
E_imag_cur: 预测的虚部电场 [N]
|
||
batch_y: 真实标签 [N, 2]
|
||
num_nodes: 节点总数(Asinh loss不使用,用于接口统一)
|
||
|
||
Returns:
|
||
loss_value: Asinh损失(不进行节点平均)
|
||
"""
|
||
true_real = batch_y[:, 0]
|
||
true_imag = batch_y[:, 1]
|
||
|
||
# 计算预测值与真实值的差
|
||
diff_real = E_real_cur - true_real
|
||
diff_imag = E_imag_cur - true_imag
|
||
|
||
# 分别计算实部和虚部的平均平方误差
|
||
mse_real = diff_real.pow(2).mean() # 实部平均平方误差
|
||
mse_imag = diff_imag.pow(2).mean() # 虚部平均平方误差
|
||
|
||
# 分别应用asinh函数
|
||
asinh_real = torch.asinh(mse_real)
|
||
asinh_imag = torch.asinh(mse_imag)
|
||
|
||
# 将实部和虚部的asinh结果相加,然后开方
|
||
loss_sum = torch.sqrt(asinh_real + asinh_imag)
|
||
|
||
return loss_sum
|
||
|
||
def compute_hybrid_loss(E_real_cur, E_imag_cur, batch_y, k_all, node_batch, B, device, matrix_dtype, epoch, num_nodes):
|
||
"""
|
||
计算Hybrid损失:始终采用MSE + 0.1*Phi的固定组合(Phi loss进行节点平均)
|
||
|
||
Args:
|
||
E_real_cur: 预测的实部电场 [N]
|
||
E_imag_cur: 预测的虚部电场 [N]
|
||
batch_y: 真实标签 [N, 2]
|
||
k_all: 样本索引 [B]
|
||
node_batch: 节点到批次的映射 [N]
|
||
B: 批次大小
|
||
device: 计算设备
|
||
matrix_dtype: 矩阵数据类型
|
||
epoch: 当前训练轮次
|
||
num_nodes: 节点总数
|
||
|
||
Returns:
|
||
tuple: (loss_value, mse_loss_sum, phi_loss_sum)
|
||
- loss_value: Hybrid损失(MSE + 0.1*Phi,进行Phi节点平均)
|
||
- mse_loss_sum: MSE loss总和(未平均)
|
||
- phi_loss_sum: Phi loss总和(未平均)
|
||
"""
|
||
# 计算MSE loss(总和,未平均)
|
||
mse_loss_sum = compute_mse_loss(E_real_cur, E_imag_cur, batch_y, num_nodes)
|
||
|
||
# 计算Phi loss(总和,未平均)
|
||
phi_loss_sum = compute_phi_loss(E_real_cur, E_imag_cur, k_all, node_batch, B, device, matrix_dtype, num_nodes)
|
||
|
||
# Phi loss进行节点平均:总Phi loss除以节点数num_nodes
|
||
phi_loss_per_node = phi_loss_sum / num_nodes
|
||
|
||
# 固定权重策略:始终使用MSE + 0.1*Phi
|
||
lambda_phi = 0.5
|
||
|
||
# 组合损失:(MSE_sum / num_nodes) + 0.1 * Phi_per_node
|
||
loss_value = (mse_loss_sum / num_nodes) + lambda_phi * phi_loss_per_node
|
||
|
||
return loss_value, mse_loss_sum, phi_loss_sum
|
||
|
||
# ==========================================
|
||
# ==========================================
|
||
# 3.5. 预训练模型加载函数
|
||
# ==========================================
|
||
def load_pretrained_model(model_dir, solver, n_iter, device, is_main_process):
|
||
"""
|
||
只加载预训练模型权重(不加载优化器、调度器等状态)
|
||
用于迁移学习:在新数据集上使用预训练模型继续训练
|
||
|
||
Args:
|
||
model_dir: 模型权重文件目录
|
||
solver: 模型
|
||
n_iter: 迭代次数
|
||
device: 设备
|
||
is_main_process: 是否为主进程
|
||
|
||
Returns:
|
||
bool: 是否成功加载
|
||
"""
|
||
if is_main_process:
|
||
print(f"📂 正在加载预训练模型权重: {model_dir}")
|
||
|
||
# 获取原始模型(去除DDP包装)
|
||
raw_model = solver.module if isinstance(solver, (DataParallel, DDP)) else solver
|
||
|
||
# 检查模型文件是否存在
|
||
all_exist = True
|
||
for i in range(n_iter):
|
||
real_path = os.path.join(model_dir, f"real_iter_{i}.pth")
|
||
imag_path = os.path.join(model_dir, f"imag_iter_{i}.pth")
|
||
if not os.path.exists(real_path) or not os.path.exists(imag_path):
|
||
all_exist = False
|
||
if is_main_process:
|
||
print(f"⚠️ 模型文件不存在: {real_path} 或 {imag_path}")
|
||
break
|
||
|
||
if not all_exist:
|
||
if is_main_process:
|
||
print(" 将从随机初始化开始训练")
|
||
return False
|
||
|
||
# 加载模型权重
|
||
try:
|
||
for i in range(n_iter):
|
||
real_path = os.path.join(model_dir, f"real_iter_{i}.pth")
|
||
imag_path = os.path.join(model_dir, f"imag_iter_{i}.pth")
|
||
|
||
real_net = raw_model.model_real.get_network(i)
|
||
imag_net = raw_model.model_imag.get_network(i)
|
||
|
||
real_net.load_state_dict(torch.load(real_path, map_location=device, weights_only=True))
|
||
imag_net.load_state_dict(torch.load(imag_path, map_location=device, weights_only=True))
|
||
|
||
if is_main_process:
|
||
print(f"✅ 预训练模型权重加载成功!")
|
||
print(f" 已加载 {n_iter} 个迭代网络的权重")
|
||
print(f" ⚠️ 注意:优化器、学习率调度器等状态已重新初始化")
|
||
|
||
return True
|
||
except Exception as e:
|
||
if is_main_process:
|
||
print(f"❌ 加载预训练模型失败: {e}")
|
||
print(" 将从随机初始化开始训练")
|
||
return False
|
||
|
||
|
||
# 4. 物理求解器封装 (核心逻辑)
|
||
# ==========================================
|
||
class PhiSAGESolver(nn.Module):
|
||
"""
|
||
将物理迭代循环封装为 Module,以便 DataParallel 可以自动分发计算。
|
||
"""
|
||
def __init__(self, input_feats, output_feats, n_iter=N_ITER):
|
||
super(PhiSAGESolver, self).__init__()
|
||
self.n_iter = n_iter
|
||
# 内部实例化两个模型
|
||
self.model_real = BuildGCNList(input_feats, output_feats, n_iter)
|
||
self.model_imag = BuildGCNList(input_feats, output_feats, n_iter)
|
||
|
||
def forward(self, data_list, epoch=None):
|
||
"""
|
||
DP 模式下,data_list 是一个列表(原本 Batch 的一部分)。
|
||
我们需要在当前 GPU 上将其 Collate 成一个 Batch,然后跑物理循环。
|
||
|
||
Args:
|
||
data_list: 数据列表
|
||
epoch: 当前训练轮次,用于hybrid loss计算(可选)
|
||
"""
|
||
# 1. 确保 data_list 中的元素都是 Data 对象
|
||
# DataParallel 可能会传递特殊格式的数据,需要处理
|
||
if not isinstance(data_list, list):
|
||
data_list = [data_list]
|
||
|
||
# 检查并转换数据格式
|
||
processed_data_list = []
|
||
for item in data_list:
|
||
if isinstance(item, Data):
|
||
# 确保 Data 对象有必要的属性,并且 k_idx 是 tensor
|
||
if not hasattr(item, 'k_idx'):
|
||
# 如果没有 k_idx,尝试从其他属性获取或设置默认值
|
||
item.k_idx = torch.tensor([0])
|
||
elif not isinstance(item.k_idx, torch.Tensor):
|
||
item.k_idx = torch.tensor([item.k_idx] if not isinstance(item.k_idx, (list, tuple)) else item.k_idx)
|
||
processed_data_list.append(item)
|
||
elif isinstance(item, tuple):
|
||
# 如果是 tuple,可能是 (data, ...) 格式,取第一个元素
|
||
if len(item) > 0 and isinstance(item[0], Data):
|
||
processed_data_list.append(item[0])
|
||
else:
|
||
raise TypeError(f"无法处理的数据格式: {type(item)}, 内容: {item}")
|
||
else:
|
||
# 尝试直接使用,如果失败会抛出异常
|
||
processed_data_list.append(item)
|
||
|
||
# 2. 在当前 GPU 上构建 Batch
|
||
# 注意:如果遇到 tupleBatch 错误,可能是 PyG 版本问题
|
||
# 尝试使用 collate 函数作为备选方案
|
||
try:
|
||
batch = Batch.from_data_list(processed_data_list)
|
||
except (AttributeError, TypeError) as e:
|
||
error_msg = str(e)
|
||
if 'stores_as' in error_msg or 'tupleBatch' in error_msg:
|
||
# 使用 collate 函数手动构建 Batch
|
||
from torch_geometric.data.collate import collate
|
||
try:
|
||
batch, slice_dict, inc_dict = collate(
|
||
Data,
|
||
processed_data_list,
|
||
increment=True,
|
||
add_batch=True,
|
||
follow_batch=[],
|
||
)
|
||
except Exception as e2:
|
||
# 如果 collate 也失败,提供更详细的错误信息
|
||
if is_main_process():
|
||
print(f"❌ Batch.from_data_list 失败: {e}")
|
||
print(f"❌ collate 也失败: {e2}")
|
||
print(f" 数据列表长度: {len(processed_data_list)}")
|
||
print(f" 第一个元素类型: {type(processed_data_list[0]) if processed_data_list else 'None'}")
|
||
if processed_data_list:
|
||
print(f" 第一个元素的属性: {dir(processed_data_list[0])}")
|
||
raise e
|
||
else:
|
||
raise e
|
||
|
||
# 确保 batch 在正确的设备上
|
||
# 在多卡情况下,DataParallel 会自动处理设备分配,batch 已经在正确的设备上
|
||
# 在单卡情况下,需要确保 batch 在模型所在的设备上
|
||
device = batch.x.device
|
||
|
||
# 安全地获取模型设备(避免在 DataParallel replica 中出错)
|
||
try:
|
||
# 尝试获取模型参数所在的设备
|
||
model_device = next(self.parameters()).device
|
||
# 如果 batch 不在模型设备上,则移动 batch(主要用于单卡情况)
|
||
if device != model_device:
|
||
batch = batch.to(model_device)
|
||
device = model_device
|
||
except (StopIteration, RuntimeError):
|
||
# 在 DataParallel 的 replica 中,参数可能不可用
|
||
# 此时 batch 已经在正确的设备上(由 DataParallel 保证),直接使用 batch 的设备
|
||
pass
|
||
|
||
# 2. 准备数据
|
||
k_all = batch.k_idx
|
||
node_batch = batch.batch
|
||
B = k_all.size(0)
|
||
|
||
# 优化:减少不必要的 clone,使用 view 或直接索引
|
||
# 节点特征:[eps_re, eps_im, r_re, r_im, Ebz_re, Ebz_im, bg_re, bg_im]
|
||
# 根据模型精度决定数据类型 - 兼容不同层类型
|
||
if hasattr(self.model_real.networks[0].gcn1, 'conv'):
|
||
# GCN层的情况
|
||
is_double = self.model_real.networks[0].gcn1.conv.lin_fusion.weight.dtype == torch.float64
|
||
elif hasattr(self.model_real.networks[0].gcn1, 'spatial_conv'):
|
||
# SpectralGCN层的情况
|
||
is_double = self.model_real.networks[0].gcn1.spatial_conv.lin_fusion.weight.dtype == torch.float64
|
||
elif hasattr(self.model_real.networks[0].gcn1, 'linear'):
|
||
# FFTLayer的情况
|
||
is_double = self.model_real.networks[0].gcn1.linear.weight.dtype == torch.float64
|
||
else:
|
||
# 默认情况
|
||
is_double = False
|
||
|
||
if is_double:
|
||
eps_feat = batch.x[:, 0:2].double() # L-BFGS阶段用double
|
||
bg_real = batch.x[:, 6].double() # 背景场实部 (不随网络更新)
|
||
bg_imag = batch.x[:, 7].double() # 背景场虚部 (不随网络更新)
|
||
current_E_real = batch.x[:, 4].double() # 初始电场实部
|
||
current_E_imag = batch.x[:, 5].double() # 初始电场虚部
|
||
else:
|
||
eps_feat = batch.x[:, 0:2].float() # Adam阶段用float
|
||
bg_real = batch.x[:, 6].float() # 背景场实部 (不随网络更新)
|
||
bg_imag = batch.x[:, 7].float() # 背景场虚部 (不随网络更新)
|
||
current_E_real = batch.x[:, 4].float() # 初始电场实部
|
||
current_E_imag = batch.x[:, 5].float() # 初始电场虚部
|
||
|
||
# 3. 优化:在物理迭代循环之前一次性加载所有矩阵到 GPU
|
||
# 这样可以在多次迭代中重复使用,避免重复的 CPU->GPU 传输(性能关键优化)
|
||
A_list = []
|
||
b_list = []
|
||
# 根据模型精度决定矩阵精度 - 兼容不同层类型
|
||
if hasattr(self.model_real.networks[0].gcn1, 'conv'):
|
||
model_dtype = self.model_real.networks[0].gcn1.conv.lin_fusion.weight.dtype
|
||
elif hasattr(self.model_real.networks[0].gcn1, 'spatial_conv'):
|
||
model_dtype = self.model_real.networks[0].gcn1.spatial_conv.lin_fusion.weight.dtype
|
||
elif hasattr(self.model_real.networks[0].gcn1, 'linear'):
|
||
model_dtype = self.model_real.networks[0].gcn1.linear.weight.dtype
|
||
else:
|
||
model_dtype = torch.float32
|
||
|
||
matrix_dtype = torch.complex128 if model_dtype == torch.float64 else torch.complex64
|
||
|
||
for b_idx in range(B):
|
||
k = int(k_all[b_idx].item())
|
||
A, b = get_Ab(k, device, matrix_dtype)
|
||
A_list.append(A)
|
||
b_list.append(b)
|
||
|
||
# 只保存当前迭代的结果,而不是所有历史
|
||
E_real_cur = current_E_real
|
||
E_imag_cur = current_E_imag
|
||
|
||
# 4. 物理迭代循环
|
||
for iter_idx in range(self.n_iter):
|
||
r_real_list = []
|
||
r_imag_list = []
|
||
|
||
# 计算残差 r = b - A*E
|
||
# 优化:使用已加载的矩阵(已在 GPU 上),避免重复传输
|
||
with torch.no_grad():
|
||
for b_idx in range(B):
|
||
mask = (node_batch == b_idx)
|
||
E_r = E_real_cur[mask]
|
||
E_i = E_imag_cur[mask]
|
||
|
||
# 使用已加载的矩阵(已在 GPU 上,无需重复传输)
|
||
A = A_list[b_idx]
|
||
b_vec = b_list[b_idx]
|
||
|
||
E_c = torch.complex(E_r, E_i)
|
||
Ax = torch.sparse.mm(A, E_c.unsqueeze(-1)).squeeze(-1)
|
||
r_c = b_vec - Ax
|
||
|
||
# 根据当前模型精度决定数据类型 - 兼容不同层类型
|
||
if hasattr(self.model_real.networks[0].gcn1, 'conv'):
|
||
use_double = self.model_real.networks[0].gcn1.conv.lin_fusion.weight.dtype == torch.float64
|
||
elif hasattr(self.model_real.networks[0].gcn1, 'spatial_conv'):
|
||
use_double = self.model_real.networks[0].gcn1.spatial_conv.lin_fusion.weight.dtype == torch.float64
|
||
elif hasattr(self.model_real.networks[0].gcn1, 'linear'):
|
||
use_double = self.model_real.networks[0].gcn1.linear.weight.dtype == torch.float64
|
||
else:
|
||
use_double = False
|
||
|
||
if use_double:
|
||
r_real_list.append(r_c.real.double())
|
||
r_imag_list.append(r_c.imag.double())
|
||
else:
|
||
r_real_list.append(r_c.real.float())
|
||
r_imag_list.append(r_c.imag.float())
|
||
|
||
# 释放中间变量(但保留矩阵 A 和 b,因为还要在下次迭代中使用)
|
||
del E_c, Ax, r_c
|
||
|
||
r_real = torch.cat(r_real_list, dim=0)
|
||
r_imag = torch.cat(r_imag_list, dim=0)
|
||
|
||
# 优化:释放中间列表
|
||
del r_real_list, r_imag_list
|
||
|
||
# 构造输入(优化:使用 view 而不是 unsqueeze,节省显存)
|
||
# 节点特征包含:[eps, r, E_current, bg] 共8个通道
|
||
x_in = torch.cat([
|
||
eps_feat, # eps_real, eps_imag [N, 2]
|
||
r_real.view(-1, 1), # r_real [N, 1]
|
||
r_imag.view(-1, 1), # r_imag [N, 1]
|
||
E_real_cur.view(-1, 1), # 当前电场实部 [N, 1] (随迭代更新)
|
||
E_imag_cur.view(-1, 1), # 当前电场虚部 [N, 1] (随迭代更新)
|
||
bg_real.view(-1, 1), # 背景场实部 [N, 1] (不随网络更新)
|
||
bg_imag.view(-1, 1) # 背景场虚部 [N, 1] (不随网络更新)
|
||
], dim=1)
|
||
|
||
# 优化:使用梯度检查点节省显存(在训练模式下)
|
||
use_checkpoint = USE_GRADIENT_CHECKPOINTING
|
||
|
||
# 使用梯度检查点
|
||
if self.training and use_checkpoint:
|
||
# 梯度检查点:在前向传播时不保存中间激活值,反向传播时重新计算
|
||
# 这会节省显存,但会增加计算时间(约20-30%)
|
||
def gcn_forward_real(x, edge_index, batch, iter_idx):
|
||
return self.model_real(x, edge_index, batch, iter_idx)
|
||
def gcn_forward_imag(x, edge_index, batch, iter_idx):
|
||
return self.model_imag(x, edge_index, batch, iter_idx)
|
||
|
||
delta_real = checkpoint(gcn_forward_real, x_in, batch.edge_index, batch.batch, iter_idx, use_reentrant=False)
|
||
delta_imag = checkpoint(gcn_forward_imag, x_in, batch.edge_index, batch.batch, iter_idx, use_reentrant=False)
|
||
else:
|
||
# 正常前向传播
|
||
delta_real = self.model_real(x_in, batch.edge_index, batch.batch, iter_idx)
|
||
delta_imag = self.model_imag(x_in, batch.edge_index, batch.batch, iter_idx)
|
||
|
||
# 展平并更新(使用 in-place 操作节省显存)
|
||
delta_real = delta_real.view(-1)
|
||
delta_imag = delta_imag.view(-1)
|
||
|
||
# 优化:直接更新,不保存历史(只保留当前值)
|
||
E_real_next = E_real_cur + delta_real
|
||
E_imag_next = E_imag_cur + delta_imag
|
||
|
||
# 优化:释放中间变量
|
||
del x_in, delta_real, delta_imag, r_real, r_imag
|
||
|
||
# 更新当前值(为下一次迭代准备)
|
||
E_real_cur = E_real_next
|
||
E_imag_cur = E_imag_next
|
||
|
||
# 优化:不在每次迭代中清理显存,避免阻塞
|
||
# 仅在阶段切换时清理显存碎片
|
||
|
||
# 5. 计算损失
|
||
num_nodes = batch.x.size(0) # 总节点数
|
||
if LOSS_TYPE == "hybrid":
|
||
# compute_hybrid_loss现在返回三个值,避免重复计算
|
||
loss_sum, mse_loss_sum, phi_loss_sum = compute_hybrid_loss(E_real_cur, E_imag_cur, batch.y, k_all, node_batch, B, device, matrix_dtype, epoch, num_nodes)
|
||
else:
|
||
# 非hybrid模式下,只计算实际需要的loss
|
||
if LOSS_TYPE == "phi":
|
||
loss_sum = compute_phi_loss(E_real_cur, E_imag_cur, k_all, node_batch, B, device, matrix_dtype, num_nodes)
|
||
elif LOSS_TYPE == "asinh":
|
||
loss_sum = compute_asinh_loss(E_real_cur, E_imag_cur, batch.y, num_nodes)
|
||
else:
|
||
# 默认使用MSE loss
|
||
loss_sum = compute_mse_loss(E_real_cur, E_imag_cur, batch.y, num_nodes)
|
||
|
||
# 非hybrid模式下不需要单独的MSE和Phi loss,使用占位符
|
||
mse_loss_sum = torch.tensor(0.0, device=device)
|
||
phi_loss_sum = torch.tensor(0.0, device=device)
|
||
|
||
# 6. 计算相对误差形式的RES loss
|
||
# RES loss需要真实标签,用于计算相对误差
|
||
true_real = batch.y[:, 0]
|
||
true_imag = batch.y[:, 1]
|
||
# RES loss = ||x^FEM - x^GCN||_2^2 / ||x^FEM||_2^2
|
||
# 其中 x^FEM 是真实解,x^GCN 是预测解
|
||
# ||x||_2^2 = sum_i |x_i|^2,计算平方的相对误差(与公式一致)
|
||
res_loss_sum = torch.tensor(0.0, device=device, dtype=loss_sum.dtype)
|
||
with torch.no_grad():
|
||
for b_idx in range(B):
|
||
mask = (node_batch == b_idx)
|
||
# 预测解 x^GCN
|
||
pred_real = E_real_cur[mask]
|
||
pred_imag = E_imag_cur[mask]
|
||
# 真实解 x^FEM
|
||
true_r = true_real[mask]
|
||
true_i = true_imag[mask]
|
||
|
||
# 计算 ||x^FEM - x^GCN||_2^2 = sum_i |true_i - pred_i|^2
|
||
# 对于复数向量,需要分别计算实部和虚部
|
||
diff_real = true_r - pred_real
|
||
diff_imag = true_i - pred_imag
|
||
# 提前取绝对值再平方
|
||
numerator = (torch.abs(diff_real).pow(2).sum() + torch.abs(diff_imag).pow(2).sum())
|
||
|
||
# 计算 ||x^FEM||_2^2 = sum_i |true_i|^2
|
||
# 提前取绝对值再平方
|
||
denominator = (torch.abs(true_r).pow(2).sum() + torch.abs(true_i).pow(2).sum())
|
||
|
||
# 避免除零,如果分母太小则使用一个小的epsilon
|
||
epsilon = 1e-10
|
||
if denominator > epsilon:
|
||
# 平方的相对误差:||x^FEM - x^GCN||_2^2 / ||x^FEM||_2^2(与公式一致)
|
||
rel_error_squared = numerator / denominator
|
||
res_loss_sum = res_loss_sum + rel_error_squared
|
||
else:
|
||
# 如果真实解范数太小,使用绝对误差的平方
|
||
res_loss_sum = res_loss_sum + numerator
|
||
|
||
# 释放中间变量
|
||
del diff_real, diff_imag, pred_real, pred_imag, true_r, true_i
|
||
|
||
# 优化:在迭代结束后释放矩阵列表(释放显存)
|
||
del A_list, b_list
|
||
|
||
# 释放变量
|
||
del E_real_cur, E_imag_cur, true_real, true_imag
|
||
|
||
# 注意:PyG DataParallel 可能不支持 tuple 返回值,会尝试将其当作 Batch 处理
|
||
# 因此返回单个 tensor:[loss_sum, res_loss_sum, num_nodes, num_samples, mse_loss_sum, phi_loss_sum]
|
||
# 这样可以避免 'tupleBatch' 错误
|
||
# num_nodes: 总节点数(用于loss的平均)
|
||
# num_samples: 总样本数(用于RES loss的平均,因为相对误差是针对每个样本的)
|
||
num_nodes = batch.x.size(0) # 总节点数(从 batch.x 获取,不需要从 E_real_cur)
|
||
num_samples = B # 样本数(batch中的图数量)
|
||
num_nodes_tensor = torch.tensor(num_nodes, dtype=torch.float32, device=device)
|
||
num_samples_tensor = torch.tensor(num_samples, dtype=torch.float32, device=device)
|
||
mse_loss_tensor = mse_loss_sum.to(dtype=torch.float32, device=device) if hasattr(mse_loss_sum, 'to') else torch.tensor(float(mse_loss_sum), dtype=torch.float32, device=device)
|
||
phi_loss_tensor = phi_loss_sum.to(dtype=torch.float32, device=device) if hasattr(phi_loss_sum, 'to') else torch.tensor(float(phi_loss_sum), dtype=torch.float32, device=device)
|
||
# 返回形状为 [6] 的 tensor: [loss_sum, res_loss_sum, num_nodes, num_samples, mse_loss_sum, phi_loss_sum]
|
||
return torch.stack([loss_sum, res_loss_sum, num_nodes_tensor, num_samples_tensor, mse_loss_tensor, phi_loss_tensor])
|
||
|
||
|
||
|
||
# ==========================================
|
||
# 5. 主程序
|
||
# ==========================================
|
||
def setup_ddp(rank, world_size):
|
||
"""设置DDP环境"""
|
||
os.environ['MASTER_ADDR'] = 'localhost'
|
||
os.environ['MASTER_PORT'] = MASTER_PORT
|
||
|
||
# 设置NCCL优化环境变量,提高分布式训练稳定性
|
||
os.environ['NCCL_TIMEOUT'] = '1800000' # 30分钟超时 (毫秒)
|
||
os.environ['NCCL_IB_DISABLE'] = '1' # 禁用IB以提高兼容性
|
||
os.environ['NCCL_SOCKET_IFNAME'] = 'lo' # 使用本地环回接口
|
||
os.environ['NCCL_DEBUG'] = 'WARN' # 设置调试级别
|
||
|
||
# 初始化进程组,设置更长的超时时间(30分钟)以避免NCCL超时
|
||
dist.init_process_group("nccl", rank=rank, world_size=world_size, timeout=timedelta(minutes=30))
|
||
|
||
# 设置当前进程的GPU
|
||
torch.cuda.set_device(rank)
|
||
device = torch.device(f'cuda:{rank}')
|
||
|
||
return device
|
||
|
||
def cleanup_ddp():
|
||
"""清理DDP环境"""
|
||
dist.destroy_process_group()
|
||
|
||
def main_worker(rank, world_size):
|
||
"""DDP训练的主工作函数"""
|
||
device = setup_ddp(rank, world_size)
|
||
|
||
# 只有主进程(rank 0)输出信息
|
||
is_main_process = (rank == 0)
|
||
|
||
# DDP环境中,每个进程只负责一个GPU
|
||
device_ids = [rank]
|
||
|
||
if is_main_process:
|
||
print(f"🚀 启动 DDP 分布式训练")
|
||
print(f" 进程 {rank}/{world_size}")
|
||
print(f" GPU: {device}")
|
||
print(f" 每个GPU的BatchSize: {TOTAL_BATCH_SIZE}")
|
||
print(f" 总BatchSize: {TOTAL_BATCH_SIZE * world_size}")
|
||
print(f" NCCL超时时间: 30分钟")
|
||
print(f" 环境变量: NCCL_TIMEOUT=1800000ms")
|
||
|
||
# 打印数据集配置信息(只在主进程中打印)
|
||
from config import DATASET_TYPE, DATA_ROOT_PATH, SCA_PREFIX
|
||
print(f"📊 全局数据集配置: {DATASET_TYPE}")
|
||
print(f" 数据根目录: {DATA_ROOT_PATH}")
|
||
print(f" 文件命名前缀: {SCA_PREFIX}")
|
||
|
||
# 2. 数据准备
|
||
data_mapping, n_total = scan_all_data(root_data_path)
|
||
|
||
# 优化:使用多进程并行构建数据集(如果数据量大)
|
||
dataset = []
|
||
|
||
# 获取所有有效的 k 索引
|
||
valid_k_list = [k for k in range(1, n_total + 1) if k in data_mapping]
|
||
|
||
# 使用串行方式构建数据集(多进程会导致共享内存问题)
|
||
# 注意:多进程传递大量 Data 对象时,Python 的 multiprocessing 使用共享内存
|
||
# 可能导致 "Too many open files" 错误,因此使用串行方式更稳定
|
||
if is_main_process:
|
||
print("构建数据集...")
|
||
print(" 串行构建数据集(稳定可靠)...")
|
||
for idx, k in enumerate(valid_k_list):
|
||
try:
|
||
data = build_graph_data(k)
|
||
if data is not None:
|
||
dataset.append(data)
|
||
# 每 100 个样本显示一次进度
|
||
if (idx + 1) % 1000 == 0 and is_main_process:
|
||
print(f" 进度: {idx + 1}/{len(valid_k_list)} ({100*(idx+1)/len(valid_k_list):.1f}%)")
|
||
except Exception as e:
|
||
if (idx + 1) % 1000 == 0 and is_main_process: # 只在显示进度时打印错误
|
||
print(f" 构建样本 {k} 失败: {e}")
|
||
pass
|
||
|
||
if len(dataset) == 0:
|
||
raise RuntimeError("数据集为空,无法进行训练!")
|
||
|
||
if is_main_process:
|
||
print(f" 数据集大小: {len(dataset)}")
|
||
train_ds, test_ds = train_test_split(dataset, test_size=0.2, random_state=42)
|
||
|
||
# # 定义测试集的编号(从1开始计数,对应数据索引)
|
||
# test_indices = [1,13,14,18,19,24,25,36,37,42,43,48,49,60,61,66,67,72,73,84,85,90,91,96]
|
||
|
||
# # 根据编号划分训练集和测试集
|
||
# train_ds = []
|
||
# test_ds = []
|
||
|
||
# for i, data in enumerate(dataset):
|
||
# # data.k_idx 存储的是数据的编号(从1开始)
|
||
# data_idx = data.k_idx.item()
|
||
# if data_idx in test_indices:
|
||
# test_ds.append(data)
|
||
# else:
|
||
# train_ds.append(data)
|
||
|
||
if is_main_process:
|
||
print(f" 训练集: {len(train_ds)}, 测试集: {len(test_ds)}")
|
||
# print(f" 测试集编号: {test_indices}")
|
||
|
||
# 【关键】使用 DataListLoader
|
||
# 注意:PyG 的 DataListLoader 可能不支持 num_workers(专为 DataParallel 设计)
|
||
# 但我们可以尝试设置,如果不支持会自动忽略
|
||
# 优化:添加 pin_memory 以加速数据传输到GPU
|
||
|
||
# Adam阶段:开启shuffle增加训练随机性,提高泛化能力
|
||
train_loader = DataListLoader(
|
||
train_ds,
|
||
batch_size=TOTAL_BATCH_SIZE,
|
||
shuffle=True, # Adam阶段开启shuffle
|
||
drop_last=True,
|
||
pin_memory=True, # 确保为True
|
||
num_workers=NUM_WORKERS, # 添加这行!关键优化
|
||
persistent_workers=True if NUM_WORKERS > 0 else False # 保持worker进程
|
||
)
|
||
|
||
# 测试阶段:关闭shuffle,确保评估结果一致性
|
||
test_loader = DataListLoader(
|
||
test_ds,
|
||
batch_size=TOTAL_BATCH_SIZE,
|
||
shuffle=False, # 测试阶段关闭shuffle
|
||
pin_memory=True,
|
||
num_workers=NUM_WORKERS, # 添加
|
||
persistent_workers=True if NUM_WORKERS > 0 else False
|
||
)
|
||
|
||
# 3. 预加载矩阵到所有 GPU
|
||
load_matrix_to_cache(data_mapping, n_total, device_ids, is_main_process)
|
||
|
||
# 【关键】DDP同步点:确保所有进程都完成矩阵预加载后再继续
|
||
# 这可以避免进程间不同步导致的NCCL心跳超时
|
||
if dist.is_initialized():
|
||
dist.barrier()
|
||
if is_main_process:
|
||
print("✅ 所有进程已完成矩阵预加载,继续训练...")
|
||
|
||
# 4. 模型初始化与 DP 包装
|
||
# Adam阶段使用float32加快速度,L-BFGS阶段切换到float64提高精度
|
||
# 节点特征包含:[eps, r, E_current, bg] 共8个通道
|
||
solver = PhiSAGESolver(input_feats=8, output_feats=1, n_iter=N_ITER).float() # Adam阶段用float32
|
||
solver.to(device)
|
||
|
||
# 优化:使用 torch.compile 加速(如果支持)
|
||
# 注意:torch.compile 需要在 DataParallel 包装之前应用
|
||
# 使用局部变量来避免修改全局变量
|
||
use_compile = USE_COMPILE
|
||
if use_compile and is_main_process:
|
||
try:
|
||
# 检查 PyTorch 版本是否支持 compile
|
||
if hasattr(torch, 'compile'):
|
||
print("✅ 启用 torch.compile 优化...")
|
||
print(" ⚠️ 注意:首次运行需要编译时间,可能较慢")
|
||
print(" ⚠️ 如果遇到错误,请将 USE_COMPILE 设置为 False")
|
||
# 使用 'reduce-overhead' 模式,适合多次调用的场景
|
||
solver = torch.compile(solver, mode='reduce-overhead')
|
||
else:
|
||
print("⚠️ PyTorch 版本不支持 torch.compile,跳过此优化")
|
||
use_compile = False
|
||
except Exception as e:
|
||
print(f"⚠️ torch.compile 失败: {e},继续使用未编译版本")
|
||
print(f" 💡 提示:如果遇到兼容性问题,请将 USE_COMPILE 设置为 False")
|
||
use_compile = False
|
||
|
||
# 打印网络维度信息
|
||
if is_main_process:
|
||
from config import NETWORK_USE_CUSTOM_DIMS, NETWORK_BASE_DIM, NETWORK_CUSTOM_DIMS, NETWORK_POOL_RATIOS
|
||
|
||
print("\n🔍 网络结构维度信息:")
|
||
print(f" 迭代次数 (n_iter): {solver.n_iter}")
|
||
print(f" 输入特征数: {solver.model_real.input_feats}")
|
||
print(f" 输出特征数: {solver.model_real.output_feats}")
|
||
|
||
# 显示配置来源
|
||
print(f" 配置来源: config.py")
|
||
if NETWORK_USE_CUSTOM_DIMS:
|
||
print(f" • 使用自定义维度: {NETWORK_CUSTOM_DIMS}")
|
||
else:
|
||
print(f" • 使用基础维度: {NETWORK_BASE_DIM} (自动计算: [{NETWORK_BASE_DIM}, {NETWORK_BASE_DIM*2}, {NETWORK_BASE_DIM*4}])")
|
||
print(f" • 池化配置: {NETWORK_POOL_RATIOS}")
|
||
|
||
try:
|
||
# 获取第一个网络来显示维度信息
|
||
first_network = solver.model_real.networks[0]
|
||
|
||
# 显示网络维度配置
|
||
if hasattr(first_network, 'gcn1') and hasattr(first_network.gcn1, 'conv') and hasattr(first_network.gcn1.conv, 'lin_fusion'):
|
||
gcn1_weight = first_network.gcn1.conv.lin_fusion.weight
|
||
gcn2_weight = first_network.gcn2.conv.lin_fusion.weight
|
||
gcn3_weight = first_network.gcn3.conv.lin_fusion.weight
|
||
|
||
print(f" GCN层实际维度:")
|
||
print(f" • gcn1: {gcn1_weight.shape[1]} → {gcn1_weight.shape[0]}")
|
||
print(f" • gcn2: {gcn2_weight.shape[1]} → {gcn2_weight.shape[0]}")
|
||
print(f" • gcn3: {gcn3_weight.shape[1]} → {gcn3_weight.shape[0]}")
|
||
print(f" 网络架构: U-Net风格 (编码器-解码器)")
|
||
|
||
# 计算参数量
|
||
total_params = sum(p.numel() for p in solver.parameters())
|
||
real_params = sum(p.numel() for p in solver.model_real.parameters())
|
||
imag_params = sum(p.numel() for p in solver.model_imag.parameters())
|
||
|
||
print(f" 参数统计:")
|
||
print(f" • 总参数量: {total_params:,} ({total_params/1e6:.2f}M)")
|
||
print(f" • Real网络: {real_params:,} 参数")
|
||
print(f" • Imag网络: {imag_params:,} 参数")
|
||
print(f" • 单迭代网络: {real_params // solver.n_iter:,} 参数")
|
||
else:
|
||
print(" ⚠️ 无法获取详细的网络维度信息")
|
||
|
||
except Exception as e:
|
||
print(f" ⚠️ 获取网络维度信息时出错: {e}")
|
||
# 仍然显示基本参数量信息
|
||
total_params = sum(p.numel() for p in solver.parameters())
|
||
print(f" 总参数量: {total_params:,} ({total_params/1e6:.2f}M)")
|
||
|
||
# DDP包装
|
||
solver = DDP(solver, device_ids=[rank], output_device=rank)
|
||
if is_main_process:
|
||
print("✅ 模型已通过 DDP 包装")
|
||
|
||
# 使用纯float32训练,无需GradScaler
|
||
|
||
# 5. 优化器
|
||
# 注意:DP 包装后,参数名会多出 .module 前缀,但不影响 optimizer 识别
|
||
# DDP优化:学习率按GPU数量线性缩放(总batch_size增大)
|
||
ddp_lr = LR # 每个GPU的基础学习率乘以GPU数量
|
||
optimizer_adam = optim.Adam(solver.parameters(), lr=ddp_lr)
|
||
|
||
# 使用ReduceLROnPlateau调度器,基于验证损失自动调整学习率
|
||
scheduler = ReduceLROnPlateau(
|
||
optimizer_adam,
|
||
mode=REDUCE_LR_MODE,
|
||
factor=REDUCE_LR_FACTOR,
|
||
patience=REDUCE_LR_PATIENCE,
|
||
min_lr=REDUCE_LR_MIN_LR
|
||
)
|
||
|
||
# 6. 训练循环
|
||
# 初始化训练状态变量
|
||
best_loss = float('inf') # 用于早停判断的最佳loss
|
||
best_epoch = -1
|
||
best_saved_loss = float('inf') # 用于保存模型判断的最佳loss(hybrid模式下200epoch后开始)
|
||
|
||
# 早停机制变量
|
||
early_stopping_counter = 0
|
||
early_stopping_best_loss = float('inf')
|
||
|
||
# 用于记录loss变化曲线
|
||
train_losses = []
|
||
test_losses = []
|
||
train_mse_losses = []
|
||
test_mse_losses = []
|
||
train_res_losses = []
|
||
test_res_losses = []
|
||
|
||
# 训练数据保存
|
||
training_data = {
|
||
'epochs': [],
|
||
'train_losses': [],
|
||
'test_losses': [],
|
||
'train_mse_losses': [],
|
||
'test_mse_losses': [],
|
||
'train_res_losses': [],
|
||
'test_res_losses': [],
|
||
'train_relative_errors': [],
|
||
'test_relative_errors': []
|
||
}
|
||
|
||
# Hybrid loss模式下的MSE和Phi loss记录
|
||
if LOSS_TYPE == "hybrid":
|
||
hybrid_loss_data = {
|
||
'epochs': [],
|
||
'train_mse_losses': [],
|
||
'train_phi_losses': [],
|
||
'test_mse_losses': [],
|
||
'test_phi_losses': []
|
||
}
|
||
|
||
# 记录训练总开始时间和上次打印时间
|
||
total_start_time = time.time()
|
||
last_print_time = total_start_time
|
||
|
||
# 加载预训练模型(如果启用)
|
||
if LOAD_PRETRAINED_MODEL:
|
||
pretrained_dir = PRETRAINED_MODEL_DIR if PRETRAINED_MODEL_DIR is not None else SAVE_DIR
|
||
load_success = load_pretrained_model(pretrained_dir, solver, N_ITER, device, is_main_process)
|
||
if load_success and is_main_process:
|
||
print("🔄 已加载预训练模型权重,优化器等状态已重新初始化")
|
||
print(" 将从epoch 0开始训练(在新数据集上)")
|
||
|
||
for epoch in range(TOTAL_EPOCHS):
|
||
solver.train()
|
||
optimizer = optimizer_adam # 只使用Adam优化器
|
||
|
||
epoch_loss_sum = 0.0
|
||
epoch_mse_sum = 0.0
|
||
epoch_res_sum = 0.0
|
||
epoch_phi_sum = 0.0 # Hybrid loss模式下的Phi loss累加器
|
||
total_nodes = 0
|
||
total_samples = 0 # 用于RES loss的平均(相对误差是针对每个样本的)
|
||
|
||
# ==========================
|
||
# Adam Training (纯float32)
|
||
# ==========================
|
||
for data_list in train_loader:
|
||
optimizer.zero_grad()
|
||
|
||
# Forward: list -> (split) -> GPUs -> (run) -> (gather) -> results
|
||
outputs = solver(data_list, epoch)
|
||
|
||
# 使用辅助函数提取 loss 和 num_nodes
|
||
batch_loss_sum, batch_res_sum, num_nodes, num_samples, batch_mse_sum, batch_phi_sum = extract_loss_and_num_nodes(outputs)
|
||
|
||
# 计算平均 Loss(Phi和Asinh loss不做节点平均,MSE loss使用节点平均)
|
||
# Hybrid loss根据当前权重决定是否节点平均
|
||
if num_nodes == 0:
|
||
continue # 跳过空批次
|
||
if LOSS_TYPE == "mse":
|
||
loss_mean = batch_loss_sum / num_nodes
|
||
else:
|
||
loss_mean = batch_loss_sum
|
||
|
||
loss_mean.backward()
|
||
optimizer.step()
|
||
|
||
epoch_loss_sum += batch_loss_sum.item()
|
||
epoch_res_sum += batch_res_sum.item()
|
||
# 只在hybrid模式下累加额外的loss分量
|
||
if LOSS_TYPE == "hybrid":
|
||
epoch_mse_sum += batch_mse_sum.item()
|
||
epoch_phi_sum += batch_phi_sum.item()
|
||
total_nodes += num_nodes
|
||
total_samples += num_samples
|
||
|
||
# 优化:每10个epoch清理一次显存
|
||
if epoch % 10 == 0:
|
||
torch.cuda.empty_cache()
|
||
|
||
|
||
# ==========================
|
||
# 日志与测试
|
||
# ==========================
|
||
num_batches = len(train_loader)
|
||
if num_batches > 0:
|
||
# 计算平均loss
|
||
if LOSS_TYPE == "mse":
|
||
# MSE loss: 除以总节点数,得到每个节点的平均loss
|
||
avg_train_loss = epoch_loss_sum / total_nodes if total_nodes > 0 else 0.0
|
||
avg_train_mse = avg_train_loss # MSE模式下MSE loss就是总loss
|
||
elif LOSS_TYPE == "hybrid":
|
||
# Hybrid loss: 除以batch数,得到每个batch的平均loss
|
||
avg_train_loss = epoch_loss_sum / num_batches
|
||
avg_train_mse = epoch_mse_sum / total_nodes if total_nodes > 0 else 0.0
|
||
else:
|
||
# Phi/Asinh loss: 除以batch数,得到每个batch的平均loss
|
||
avg_train_loss = epoch_loss_sum / num_batches
|
||
avg_train_mse = 0.0 # 非hybrid模式下不计算MSE loss
|
||
else:
|
||
avg_train_loss = 0.0
|
||
avg_train_mse = 0.0
|
||
|
||
# RES Loss 是 Sum of Relative Errors,所以除以 total_samples (总图数) 是对的
|
||
avg_train_res = epoch_res_sum / total_samples if total_samples > 0 else 0.0
|
||
|
||
# 计算平均Phi loss(在hybrid模式下)
|
||
if LOSS_TYPE == "hybrid":
|
||
avg_train_phi = epoch_phi_sum / total_nodes if total_nodes > 0 else 0.0
|
||
else:
|
||
avg_train_phi = 0.0
|
||
|
||
# ==========================
|
||
# 测试集评估 (同理修正)
|
||
# ==========================
|
||
solver.eval()
|
||
test_loss_sum = 0.0
|
||
test_mse_sum = 0.0
|
||
test_res_sum = 0.0
|
||
test_phi_sum = 0.0 # Hybrid loss模式下的Phi loss累加器
|
||
test_total_nodes = 0
|
||
test_total_samples = 0
|
||
test_num_batches = len(test_loader)
|
||
|
||
with torch.no_grad():
|
||
# 评估阶段(纯float32)
|
||
for data_list in test_loader:
|
||
outputs = solver(data_list, epoch)
|
||
batch_loss_sum, batch_res_sum, num_nodes, num_samples, batch_mse_sum, batch_phi_sum = extract_loss_and_num_nodes(outputs)
|
||
|
||
if num_nodes == 0:
|
||
continue
|
||
|
||
# 累加基本loss
|
||
test_res_sum += batch_res_sum.item() # 累加RES loss
|
||
test_loss_sum += batch_loss_sum.item() * num_nodes # 转换为总损失再累加
|
||
|
||
# 只在hybrid模式下累加额外的loss分量
|
||
if LOSS_TYPE == "hybrid":
|
||
test_mse_sum += batch_mse_sum.item() # 已经是总损失
|
||
test_phi_sum += batch_phi_sum.item() # 已经是总损失
|
||
test_total_nodes += num_nodes
|
||
test_total_samples += num_samples
|
||
test_num_batches += 1
|
||
|
||
if test_num_batches > 0:
|
||
# 按总节点数平均loss
|
||
if LOSS_TYPE == "mse":
|
||
# MSE loss: 除以总节点数,得到每个节点的平均loss
|
||
avg_test_loss = test_loss_sum / test_total_nodes if test_total_nodes > 0 else 0.0
|
||
avg_test_mse = avg_test_loss # MSE模式下MSE loss就是总loss
|
||
elif LOSS_TYPE == "hybrid":
|
||
# Hybrid loss: 除以总节点数
|
||
avg_test_loss = test_loss_sum / test_total_nodes if test_total_nodes > 0 else 0.0
|
||
avg_test_mse = test_mse_sum / test_total_nodes if test_total_nodes > 0 else 0.0
|
||
else:
|
||
# Phi/Asinh loss: 除以总节点数
|
||
avg_test_loss = test_loss_sum / test_total_nodes if test_total_nodes > 0 else 0.0
|
||
avg_test_mse = 0.0 # 非hybrid模式下不计算MSE loss
|
||
else:
|
||
avg_test_loss = 0.0
|
||
avg_test_mse = 0.0
|
||
|
||
avg_test_res = test_res_sum / test_total_samples if test_total_samples > 0 else 0.0
|
||
|
||
# 计算平均Phi loss(在hybrid模式下)
|
||
if LOSS_TYPE == "hybrid":
|
||
avg_test_phi = test_phi_sum / test_total_nodes if test_total_nodes > 0 else 0.0
|
||
else:
|
||
avg_test_phi = 0.0
|
||
|
||
# 使用ReduceLROnPlateau调度器
|
||
scheduler.step(avg_test_loss)
|
||
|
||
# Hybrid模式下从一开始就记录最佳loss并保存模型
|
||
if LOSS_TYPE == "hybrid":
|
||
if avg_test_loss < best_saved_loss:
|
||
best_saved_loss = avg_test_loss
|
||
best_epoch = epoch
|
||
|
||
# 保存模型
|
||
raw_model = solver.module if isinstance(solver, (DataParallel, DDP)) else solver
|
||
os.makedirs(SAVE_DIR, exist_ok=True)
|
||
|
||
# 保存模型权重
|
||
n_iter = raw_model.n_iter
|
||
for i in range(n_iter):
|
||
torch.save(raw_model.model_real.get_network(i).state_dict(),
|
||
os.path.join(SAVE_DIR, f"real_iter_{i}.pth"))
|
||
torch.save(raw_model.model_imag.get_network(i).state_dict(),
|
||
os.path.join(SAVE_DIR, f"imag_iter_{i}.pth"))
|
||
else:
|
||
# 非hybrid模式,正常保存逻辑
|
||
if avg_test_loss < best_saved_loss:
|
||
best_saved_loss = avg_test_loss
|
||
best_epoch = epoch
|
||
|
||
# 保存模型
|
||
raw_model = solver.module if isinstance(solver, (DataParallel, DDP)) else solver
|
||
os.makedirs(SAVE_DIR, exist_ok=True)
|
||
|
||
# 保存模型权重
|
||
n_iter = raw_model.n_iter
|
||
for i in range(n_iter):
|
||
torch.save(raw_model.model_real.get_network(i).state_dict(),
|
||
os.path.join(SAVE_DIR, f"real_iter_{i}.pth"))
|
||
torch.save(raw_model.model_imag.get_network(i).state_dict(),
|
||
os.path.join(SAVE_DIR, f"imag_iter_{i}.pth"))
|
||
|
||
# 记录loss用于绘图
|
||
train_losses.append(avg_train_loss)
|
||
test_losses.append(avg_test_loss)
|
||
train_mse_losses.append(avg_train_mse)
|
||
test_mse_losses.append(avg_test_mse)
|
||
train_res_losses.append(avg_train_res)
|
||
test_res_losses.append(avg_test_res)
|
||
|
||
# 保存到训练数据字典
|
||
training_data['epochs'].append(epoch)
|
||
training_data['train_losses'].append(avg_train_loss)
|
||
training_data['test_losses'].append(avg_test_loss)
|
||
training_data['train_mse_losses'].append(avg_train_mse)
|
||
training_data['test_mse_losses'].append(avg_test_mse)
|
||
training_data['train_res_losses'].append(avg_train_res)
|
||
training_data['test_res_losses'].append(avg_test_res)
|
||
|
||
# 保存hybrid loss数据
|
||
if LOSS_TYPE == "hybrid":
|
||
hybrid_loss_data['epochs'].append(epoch)
|
||
hybrid_loss_data['train_mse_losses'].append(avg_train_mse)
|
||
hybrid_loss_data['train_phi_losses'].append(avg_train_phi)
|
||
hybrid_loss_data['test_mse_losses'].append(avg_test_mse)
|
||
hybrid_loss_data['test_phi_losses'].append(avg_test_phi)
|
||
|
||
# 全局早停检查(针对所有阶段)
|
||
if epoch >= EARLY_STOPPING_START_EPOCH:
|
||
early_stopping_enabled = True
|
||
if avg_test_loss < early_stopping_best_loss - EARLY_STOPPING_MIN_DELTA:
|
||
# 有显著改善,重置计数器
|
||
early_stopping_best_loss = avg_test_loss
|
||
early_stopping_counter = 0
|
||
else:
|
||
# 无显著改善,计数器加1
|
||
early_stopping_counter += 1
|
||
|
||
# 检查是否达到早停条件
|
||
if early_stopping_counter >= EARLY_STOPPING_PATIENCE:
|
||
if is_main_process:
|
||
print(f"🛑 全局早停激活!")
|
||
print(f" 连续{EARLY_STOPPING_PATIENCE}个epoch无显著改善")
|
||
print(f" 最小改善阈值: {EARLY_STOPPING_MIN_DELTA:.0e}")
|
||
print(f" 当前loss: {avg_test_loss:.6e}")
|
||
print(f" 最佳loss: {early_stopping_best_loss:.6e} (epoch {best_epoch})")
|
||
break
|
||
else:
|
||
# 预热阶段,跟踪最佳loss但不触发早停
|
||
if avg_test_loss < early_stopping_best_loss:
|
||
early_stopping_best_loss = avg_test_loss
|
||
|
||
|
||
|
||
if epoch % EPOCH_PRINT == 0 and is_main_process:
|
||
current_time = time.time()
|
||
interval_time = current_time - last_print_time
|
||
last_print_time = current_time
|
||
|
||
# 为hybrid loss添加权重信息
|
||
loss_info = f"Train Loss: {avg_train_loss:.6e} | Test Loss: {avg_test_loss:.6e}"
|
||
|
||
print(f"Epoch {epoch:4d} | {loss_info} | "
|
||
f"Train RelErr: {avg_train_res:.6e} | Test RelErr: {avg_test_res:.6e} | "
|
||
f"Interval: {interval_time:.1f}s")
|
||
|
||
# 训练结束,输出总耗时
|
||
if is_main_process:
|
||
total_time = time.time() - total_start_time
|
||
hours = int(total_time // 3600)
|
||
minutes = int((total_time % 3600) // 60)
|
||
seconds = int(total_time % 60)
|
||
print(f"\n✅ 训练完成!总耗时: {hours:02d}:{minutes:02d}:{seconds:02d} ({total_time:.1f}秒)")
|
||
|
||
# 输出最佳模型信息
|
||
# 检查是否有模型被保存(通过best_epoch或best_saved_loss判断)
|
||
if best_epoch >= 0 and best_saved_loss < float('inf'):
|
||
print(f"\n🏆 最佳模型:")
|
||
print(f" 🎯 Epoch: {best_epoch}")
|
||
print(f" 📊 Test Loss: {best_saved_loss:.6e}")
|
||
print(f" 💾 模型已保存到: {SAVE_DIR}")
|
||
else:
|
||
print("⚠️ 警告:未找到有效的模型(可能训练失败)")
|
||
print(f" 📂 模型保存目录: {SAVE_DIR}")
|
||
|
||
# 只在主进程中保存结果和生成图表
|
||
if is_main_process:
|
||
# 保存训练数据
|
||
training_data_path = os.path.join(OUTPUT_DIR, "training_data.json")
|
||
save_training_data(training_data, training_data_path)
|
||
|
||
# 生成训练曲线
|
||
curve_pdf_path = os.path.join(OUTPUT_DIR, "training_curve.pdf")
|
||
curve_svg_path = os.path.join(OUTPUT_DIR, "training_curve.svg")
|
||
plot_training_curve(train_losses, test_losses, curve_pdf_path)
|
||
|
||
# 生成MSE和RES loss曲线
|
||
mse_res_path = os.path.join(OUTPUT_DIR, "mse_res_loss.svg")
|
||
plot_mse_res_loss(train_mse_losses, test_mse_losses, train_res_losses, test_res_losses, mse_res_path)
|
||
|
||
# 保存hybrid loss的MSE和Phi loss到txt文件(每50轮保存一次)
|
||
if LOSS_TYPE == "hybrid":
|
||
hybrid_loss_file = os.path.join(OUTPUT_DIR, "hybrid_loss_components.txt")
|
||
with open(hybrid_loss_file, 'w') as f:
|
||
f.write("Hybrid Loss Components (MSE + 1*Phi) - Every 50 epochs\n")
|
||
f.write("="*60 + "\n")
|
||
f.write("Epoch\tTrain_MSE\tTrain_Phi\tTest_MSE\tTest_Phi\n")
|
||
# 每50轮保存一次
|
||
for i, epoch in enumerate(hybrid_loss_data['epochs']):
|
||
if epoch % 50 == 0: # 每50轮保存一次
|
||
f.write(f"{epoch}\t{hybrid_loss_data['train_mse_losses'][i]:.6e}\t")
|
||
f.write(f"{hybrid_loss_data['train_phi_losses'][i]:.6e}\t")
|
||
f.write(f"{hybrid_loss_data['test_mse_losses'][i]:.6e}\t")
|
||
f.write(f"{hybrid_loss_data['test_phi_losses'][i]:.6e}\n")
|
||
|
||
print(f"✅ Hybrid loss分量已保存到: {hybrid_loss_file}")
|
||
|
||
# 计算相对误差分布
|
||
if is_main_process:
|
||
print("\n正在计算相对误差分布...")
|
||
raw_model = solver.module if isinstance(solver, (DataParallel, DDP)) else solver
|
||
# 根据模型精度决定矩阵精度 - 兼容不同层类型
|
||
if hasattr(raw_model.model_real.networks[0].gcn1, 'conv'):
|
||
model_precision = raw_model.model_real.networks[0].gcn1.conv.lin_fusion.weight.dtype
|
||
elif hasattr(raw_model.model_real.networks[0].gcn1, 'spatial_conv'):
|
||
model_precision = raw_model.model_real.networks[0].gcn1.spatial_conv.lin_fusion.weight.dtype
|
||
elif hasattr(raw_model.model_real.networks[0].gcn1, 'linear'):
|
||
model_precision = raw_model.model_real.networks[0].gcn1.linear.weight.dtype
|
||
else:
|
||
model_precision = torch.float32
|
||
|
||
matrix_dtype = torch.complex128 if model_precision == torch.float64 else torch.complex64
|
||
|
||
train_relative_errors = compute_relative_errors(solver, train_loader, data_mapping, device, matrix_dtype)
|
||
test_relative_errors = compute_relative_errors(solver, test_loader, data_mapping, device, matrix_dtype)
|
||
|
||
# 保存相对误差数据
|
||
training_data['train_relative_errors'] = train_relative_errors
|
||
training_data['test_relative_errors'] = test_relative_errors
|
||
|
||
# 计算MSE loss分布(使用训练过程中的MSE loss值)
|
||
if is_main_process:
|
||
print("\n正在生成MSE loss分布图...")
|
||
train_mse_samples = [loss for loss in train_mse_losses for _ in range(10)] # 重复值以获得更好的分布
|
||
test_mse_samples = [loss for loss in test_mse_losses for _ in range(10)] # 重复值以获得更好的分布
|
||
|
||
# 生成MSE loss分布图
|
||
mse_dist_path = os.path.join(OUTPUT_DIR, "mse_loss_distribution.svg")
|
||
plot_mse_loss_distribution(train_mse_samples, test_mse_samples, mse_dist_path)
|
||
|
||
def main():
|
||
"""主函数:启动DDP训练"""
|
||
# 检查是否有可用的GPU
|
||
if not torch.cuda.is_available():
|
||
print("❌ 未检测到CUDA GPU,无法进行DDP训练")
|
||
return
|
||
|
||
world_size = torch.cuda.device_count()
|
||
if world_size == 0:
|
||
print("❌ 未检测到任何GPU")
|
||
return
|
||
|
||
print(f"🚀 启动DDP训练,使用 {world_size} 个GPU")
|
||
|
||
# 使用spawn方式启动多进程
|
||
try:
|
||
mp.spawn(main_worker, args=(world_size,), nprocs=world_size, join=True)
|
||
except KeyboardInterrupt:
|
||
print("\n⚠️ 训练被用户中断")
|
||
except Exception as e:
|
||
print(f"❌ DDP训练失败: {e}")
|
||
raise
|
||
|
||
if __name__ == "__main__":
|
||
main() |