Files
trading_gnn/src/amd/optimizations.py
T
fegger a763ab0774 Add AMD GPU detection, replace LSTM with MLP, and fix signal generation logic
- Detect AMD GPUs via ROCm device name in config
- Replace single-timestep LSTM in GNN model with leaner post-GNN MLP
- Pass edge_attr through AMDGATConv propagate and use ones for self-loops
- Fix live trading to sell existing positions on negative signals instead
  of skipping them entirely
- Use per-file try/except in data pipeline and batch SQLite inserts
- Import torch directly in backtester instead of dynamic __import__
- Update AMP autocast import for PyTorch 2.0+ compatibility
2026-05-26 14:36:25 +02:00

344 lines
12 KiB
Python

import logging
import time
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch_geometric.nn import MessagePassing
from torch_geometric.utils import softmax
from config import config
logger = logging.getLogger(__name__)
class AMDOptimizer:
"""
AMD-specific optimizations for PyTorch models
"""
def __init__(self):
self.device = torch.device(config.DEVICE)
self._configure_rocm()
def _configure_rocm(self):
"""Configure ROCm for optimal performance"""
if config.DEVICE == "cuda" and config.AMD_GPU:
try:
# Set ROCm optimization level
torch.backends.hip.set_optimization_level(config.ROCM_OPT_LEVEL)
# Enable memory efficient attention if available
try:
from xformers.ops import MemoryEfficientAttentionFlashAttentionOp
torch.backends.cuda.enable_flash_sdp(True)
logger.info("Enabled Flash Attention for AMD GPU")
except ImportError:
logger.warning("xformers not available, using standard attention")
# Configure memory limits
total_memory = torch.cuda.get_device_properties(0).total_memory
memory_limit = int(total_memory * config.GPU_MEMORY_LIMIT)
torch.cuda.set_per_process_memory_fraction(config.GPU_MEMORY_LIMIT, 0)
logger.info(
f"Configured ROCm with optimization level {config.ROCM_OPT_LEVEL}"
)
logger.info(
f"GPU Memory: {total_memory / 1024**3:.2f}GB, Limit: {memory_limit / 1024**3:.2f}GB"
)
except Exception as e:
logger.error(f"Error configuring ROCm: {str(e)}")
def optimize_model(self, model: nn.Module):
"""Apply AMD-specific optimizations to a model"""
model = model.to(self.device)
if config.DEVICE != "cuda" or not config.AMD_GPU:
return model
try:
# Apply memory optimizations
model = self._apply_memory_optimizations(model)
logger.info("Applied AMD optimizations to model")
return model
except Exception as e:
logger.error(f"Error optimizing model: {str(e)}")
return model
def _apply_mixed_precision(self, model: nn.Module):
"""Mixed precision is handled via torch.autocast in the training loop.
Permanently casting parameters causes numerical instability; this is a no-op."""
logger.info(
f"Mixed precision ({config.PRECISION}) handled via torch.autocast in training loop"
)
return model
def _apply_memory_optimizations(self, model: nn.Module):
"""Apply memory optimizations to the model"""
# Enable gradient checkpointing for memory efficiency
if (
hasattr(model, "supports_gradient_checkpointing")
and model.supports_gradient_checkpointing
):
model.gradient_checkpointing_enable()
logger.info("Enabled gradient checkpointing")
# Apply activation checkpointing to specific modules
for name, module in model.named_modules():
if isinstance(module, (nn.LSTM, nn.GRU)):
module.activation_checkpointing = True
return model
def get_precision_dtype(self):
"""Get the precision dtype for mixed precision training"""
if config.PRECISION == "fp16":
return torch.float16
elif config.PRECISION == "bf16":
return torch.bfloat16
else:
return torch.float32
def benchmark_model(self, model: nn.Module, input_data, num_runs: int = 100):
"""Benchmark model performance on AMD GPU"""
if config.DEVICE != "cuda":
logger.warning("Benchmarking only supported on GPU")
return {}
try:
# Warm up
for _ in range(10):
_ = model(input_data)
# Benchmark inference
start_time = time.time()
for _ in range(num_runs):
with torch.no_grad():
_ = model(input_data)
inference_time = (time.time() - start_time) / num_runs
# Benchmark training
model.train()
optimizer = torch.optim.Adam(model.parameters(), lr=config.LEARNING_RATE)
criterion = nn.MSELoss()
start_time = time.time()
for _ in range(num_runs):
optimizer.zero_grad()
out = model(input_data)
loss = criterion(out, torch.randn_like(out))
loss.backward()
optimizer.step()
training_time = (time.time() - start_time) / num_runs
# Memory usage
memory_allocated = torch.cuda.memory_allocated(0)
max_memory = torch.cuda.max_memory_allocated(0)
return {
"inference_time": inference_time,
"training_time": training_time,
"throughput_inference": 1 / inference_time,
"throughput_training": 1 / training_time,
"memory_allocated": memory_allocated,
"max_memory": max_memory,
"memory_usage_percent": (
memory_allocated / torch.cuda.get_device_properties(0).total_memory
)
* 100,
}
except Exception as e:
logger.error(f"Error benchmarking model: {str(e)}")
return {}
class AMDSparseAttention(nn.Module):
"""
Sparse attention implementation optimized for AMD GPUs
"""
def __init__(self, embed_dim, num_heads, dropout=0.1):
super().__init__()
self.embed_dim = embed_dim
self.num_heads = num_heads
self.head_dim = embed_dim // num_heads
self.scaling = self.head_dim**-0.5
self.qkv_proj = nn.Linear(embed_dim, embed_dim * 3)
self.out_proj = nn.Linear(embed_dim, embed_dim)
self.dropout = nn.Dropout(dropout)
# Initialize weights
self._init_weights()
def _init_weights(self):
nn.init.xavier_uniform_(self.qkv_proj.weight)
nn.init.xavier_uniform_(self.out_proj.weight)
nn.init.zeros_(self.qkv_proj.bias)
nn.init.zeros_(self.out_proj.bias)
def forward(self, x, mask=None):
batch_size, seq_len, embed_dim = x.size()
# Project queries, keys, values
qkv = self.qkv_proj(x)
q, k, v = qkv.chunk(3, dim=-1)
# Reshape for multi-head attention
q = q.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
k = k.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
v = v.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
# Compute attention scores
attn_scores = torch.matmul(q, k.transpose(-2, -1)) * self.scaling
# Apply mask if provided
if mask is not None:
attn_scores = attn_scores.masked_fill(mask == 0, float("-inf"))
# Compute attention weights
attn_weights = F.softmax(attn_scores, dim=-1)
attn_weights = self.dropout(attn_weights)
# Apply attention to values
output = torch.matmul(attn_weights, v)
# Concatenate heads
output = (
output.transpose(1, 2).contiguous().view(batch_size, seq_len, embed_dim)
)
# Final projection
output = self.out_proj(output)
return output
class AMDGATConv(MessagePassing):
"""
GATConv implementation optimized for AMD GPUs
"""
def __init__(
self,
in_channels,
out_channels,
heads=1,
concat=True,
dropout=0.6,
add_self_loops=True,
):
super().__init__(aggr="add", node_dim=0)
self.in_channels = in_channels
self.out_channels = out_channels
self.heads = heads
self.concat = concat
self.dropout = dropout
self.add_self_loops = add_self_loops
# Linear transformations for each head
self.lin_src = nn.Parameter(torch.Tensor(in_channels, heads * out_channels))
self.lin_dst = nn.Parameter(torch.Tensor(in_channels, heads * out_channels))
# Attention parameters
self.att_src = nn.Parameter(torch.Tensor(1, heads, out_channels))
self.att_dst = nn.Parameter(torch.Tensor(1, heads, out_channels))
# Bias
self.bias = nn.Parameter(torch.Tensor(heads * out_channels))
# Initialize weights
self.reset_parameters()
def reset_parameters(self):
nn.init.xavier_uniform_(self.lin_src)
nn.init.xavier_uniform_(self.lin_dst)
nn.init.xavier_uniform_(self.att_src)
nn.init.xavier_uniform_(self.att_dst)
nn.init.zeros_(self.bias)
def forward(self, x, edge_index, edge_attr=None, size=None):
# Linear transformation
if size is None and torch.is_tensor(x):
x_src = x_dst = torch.matmul(x, self.lin_src).view(
-1, self.heads, self.out_channels
)
else:
x_src, x_dst = x[0], x[1]
x_src = torch.matmul(x_src, self.lin_src).view(
-1, self.heads, self.out_channels
)
x_dst = torch.matmul(x_dst, self.lin_dst).view(
-1, self.heads, self.out_channels
)
# Add self loops if needed
if self.add_self_loops:
num_nodes = x_src.size(0)
edge_index, edge_attr = self._add_self_loops(
edge_index, edge_attr, num_nodes
)
# Compute attention coefficients
alpha_src = (x_src * self.att_src).sum(dim=-1)
alpha_dst = (x_dst * self.att_dst).sum(dim=-1)
alpha = (alpha_src, alpha_dst)
# Propagate — pass edge_attr so correlation weights reach message()
out = self.propagate(edge_index, x=(x_src, x_dst), alpha=alpha, edge_attr=edge_attr, size=size)
# Concatenate or average heads
if self.concat:
out = out.view(-1, self.heads * self.out_channels)
else:
out = out.mean(dim=1)
# Add bias
out = out + self.bias
return out
def _add_self_loops(self, edge_index, edge_attr, num_nodes):
# Add self loops to edge_index
loop_index = torch.arange(
0, num_nodes, dtype=torch.long, device=edge_index.device
)
loop_index = loop_index.unsqueeze(0).repeat(2, 1)
if edge_attr is not None:
# Self-loop weight = 1.0 so self-messages are not zeroed out
loop_attr = edge_attr.new_ones((num_nodes,) + edge_attr.size()[1:])
edge_attr = torch.cat([edge_attr, loop_attr], dim=0)
edge_index = torch.cat([edge_index, loop_index], dim=1)
return edge_index, edge_attr
def propagate(self, edge_index, size=None, **kwargs):
return super().propagate(edge_index, size=size, **kwargs)
def message(self, x_j, alpha_j, alpha_i, edge_attr, index, ptr, size_i):
# Compute attention weights
alpha = alpha_j + alpha_i
alpha = F.leaky_relu(alpha, negative_slope=0.2)
alpha = self._softmax(alpha, index, ptr, size_i)
alpha = F.dropout(alpha, p=self.dropout, training=self.training)
# (E, heads, out_channels) scaled by attention
msg = x_j * alpha.unsqueeze(-1)
# Scale by edge correlation weight: (E, 1) → broadcasts to (E, heads, out_channels)
if edge_attr is not None:
msg = msg * edge_attr.unsqueeze(1)
return msg
def _softmax(self, src, index, ptr, num_nodes):
# Memory-efficient softmax
return softmax(src, index, ptr, num_nodes)