Compare commits

...

10 Commits

Author SHA1 Message Date
52b8680d95 Enable InfiniBand for TensorFlow distributed training
- Set NCCL IB env vars before TF import (IB_DISABLE=0, GPU Direct RDMA)
- Use NCCL_DEBUG_FILE approach for transport logging (same as PyTorch DAG)
- Add IB pre-flight diagnostics (dev/infiniband, sys/class/infiniband, ibv_devinfo)
- Add per-epoch logging with timing for all workers
- Add NCCL transport summary after training (NET/IB, GPU Direct, Channel info)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-16 16:47:23 +03:00
a33888dbd1 Use NCCL_DEBUG_FILE for transport logging
Replace os.dup2 stderr redirect with NCCL_DEBUG_FILE to avoid
interfering with NCCL's internal logging. NCCL writes TRACE output
to a temp file, then we filter and print only transport-related
lines (NET/IB, GPU Direct, Channel, via NET/Socket).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-16 16:38:58 +03:00
71b603eb1e Set NCCL_DEBUG=TRACE for transport visibility
NET/IB transport lines only appear at TRACE level. Training output
remains clean with per-epoch summaries only.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-16 16:34:11 +03:00
dc2ffc270c Restore NCCL INFO debug level, remove per-batch noise
TRACE was too verbose. INFO shows clean transport selection lines
(NET/IB, GPU Direct RDMA status) for both ranks via stderr redirect.
Per-batch logging removed; per-epoch summary kept with timing.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-16 16:27:45 +03:00
fdb3e590a6 Redirect NCCL stderr to stdout, add verbose per-batch logging
- Redirect stderr to stdout via dup2 so Airflow captures NCCL TRACE
  output for all ranks (not just rank 0)
- Log per-batch loss for all ranks
- Log per-epoch summary with local/global avg loss and timing
- Log total training time

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-16 16:24:59 +03:00
0a43170680 update dags 2026-04-16 16:19:46 +03:00
dad7b2cd1a Enable GPU Direct RDMA over IB, restore INFO debug level
Set NCCL_IB_GPU_DIRECT_RDMA_MAX_DISTANCE=6 to allow GPU Direct RDMA
despite the 6-hop PCI distance between GPU (bus 0) and IB HCA (bus 6).
Restore NCCL_DEBUG=INFO since IB transport is confirmed working.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-16 16:18:18 +03:00
917634cc74 Add IB diagnostics and NCCL TRACE debug for IB transport detection
- Set NCCL_DEBUG=TRACE with NET,COLL subsystem filter
- Add pre-flight checks: /dev/infiniband, /sys/class/infiniband, ibv_devinfo, RDMA verbs
- Report whether IB devices are visible inside the container

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-16 16:14:04 +03:00
84ca598e3a Enable InfiniBand for distributed PyTorch training
Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-16 16:06:42 +03:00
6f88661b8b Revert to ethernet - IB PKEY misconfigured in subnet manager
Root cause: Subnet manager assigns different PKEYs to ports
- test-1 (LID 0x44): Full membership PKEYs (0xa001, 0x9001)
- test-2 (LID 0x7d): Limited membership PKEY 0x7fff only

This causes MTU mismatch (4092 vs 2044) and NCCL failure.
Cluster admin needs to configure opensm partitions to fix.

Ethernet is working and stable.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-15 18:45:54 +03:00
2 changed files with 123 additions and 17 deletions

View File

@@ -38,19 +38,18 @@ def run_training_node_func(rank, world_size):
import socket import socket
from datetime import datetime from datetime import datetime
import time import time
import sys
import tempfile
# CRITICAL: Set NCCL environment variables BEFORE importing torch # CRITICAL: Set NCCL environment variables BEFORE importing torch
# This ensures NCCL reads these settings during initialization nccl_log_file = tempfile.mktemp(suffix='.nccl.log')
os.environ['NCCL_DEBUG'] = 'INFO' os.environ['NCCL_DEBUG'] = 'TRACE'
os.environ['NCCL_DEBUG_FILE'] = nccl_log_file
os.environ['NCCL_DEBUG_SUBSYS'] = 'NET'
os.environ['NCCL_TIMEOUT'] = str(NCCL_TIMEOUT) os.environ['NCCL_TIMEOUT'] = str(NCCL_TIMEOUT)
os.environ['TORCH_NCCL_BLOCKING_WAIT'] = '1' # Updated naming os.environ['TORCH_NCCL_BLOCKING_WAIT'] = '1'
os.environ['NCCL_IB_DISABLE'] = '0' # Enable IB os.environ['NCCL_IB_DISABLE'] = '0' # Enable InfiniBand
os.environ['NCCL_SOCKET_IFNAME'] = 'eth0' # Fallback to eth0 os.environ['NCCL_IB_GPU_DIRECT_RDMA_MAX_DISTANCE'] = '6' # GPU at bus 0, IB at bus 6
os.environ['NCCL_IB_GID_INDEX'] = '3' # Use RoCE v2 (GID index 3)
os.environ['NCCL_IB_TC'] = '106' # Traffic class for RoCE
os.environ['NCCL_IB_TIMEOUT'] = '22' # IB timeout (higher = more tolerant)
os.environ['NCCL_IB_RETRY_CNT'] = '7' # IB retry count
os.environ['NCCL_NET_GDR_LEVEL'] = '5' # Enable GPUDirect RDMA
os.environ['NCCL_P2P_DISABLE'] = '0' # Enable P2P os.environ['NCCL_P2P_DISABLE'] = '0' # Enable P2P
os.environ['NCCL_IGNORE_CPU_AFFINITY'] = '1' # Better compatibility os.environ['NCCL_IGNORE_CPU_AFFINITY'] = '1' # Better compatibility
@@ -138,8 +137,6 @@ def run_training_node_func(rank, world_size):
print(f" RANK: {rank}") print(f" RANK: {rank}")
print(f" WORLD_SIZE: {world_size}") print(f" WORLD_SIZE: {world_size}")
print(f" NCCL_IB_DISABLE: {os.environ.get('NCCL_IB_DISABLE')} (IB enabled)") print(f" NCCL_IB_DISABLE: {os.environ.get('NCCL_IB_DISABLE')} (IB enabled)")
print(f" NCCL_IB_GID_INDEX: {os.environ.get('NCCL_IB_GID_INDEX')}")
print(f" NCCL_IB_TIMEOUT: {os.environ.get('NCCL_IB_TIMEOUT')}")
# STEP 3.5: Pre-flight checks # STEP 3.5: Pre-flight checks
print(f"[{rank}] Running pre-flight checks...") print(f"[{rank}] Running pre-flight checks...")
@@ -163,9 +160,48 @@ def run_training_node_func(rank, world_size):
print(f"[{rank}] Found IB interfaces:") print(f"[{rank}] Found IB interfaces:")
for line in ib_lines[:5]: for line in ib_lines[:5]:
print(f" {line.strip()}") print(f" {line.strip()}")
else:
print(f"[{rank}] No IB interfaces found via 'ip addr'")
except Exception as e: except Exception as e:
print(f"[{rank}] Warning: Could not list interfaces: {e}") print(f"[{rank}] Warning: Could not list interfaces: {e}")
# Check /dev/infiniband devices
print(f"[{rank}] Checking IB device access...")
ib_dev_path = '/dev/infiniband'
if os.path.exists(ib_dev_path):
ib_devices = os.listdir(ib_dev_path)
print(f"[{rank}] /dev/infiniband contents: {ib_devices if ib_devices else 'EMPTY'}")
else:
print(f"[{rank}] /dev/infiniband does NOT exist - IB devices not mounted in container")
# Check /sys/class/infiniband
ib_sys_path = '/sys/class/infiniband'
if os.path.exists(ib_sys_path):
ib_sys_devices = os.listdir(ib_sys_path)
print(f"[{rank}] /sys/class/infiniband contents: {ib_sys_devices if ib_sys_devices else 'EMPTY'}")
else:
print(f"[{rank}] /sys/class/infiniband does NOT exist")
# Try ibv_devinfo
try:
result = subprocess.run(['ibv_devinfo'], capture_output=True, text=True, timeout=10)
print(f"[{rank}] ibv_devinfo output:")
for line in result.stdout.split('\n')[:20]:
print(f" {line}")
if result.returncode != 0:
print(f"[{rank}] ibv_devinfo stderr: {result.stderr.strip()}")
except FileNotFoundError:
print(f"[{rank}] ibv_devinfo not found - rdma-core tools not installed")
except Exception as e:
print(f"[{rank}] ibv_devinfo failed: {e}")
# Check RDMA devices via sysfs
rdma_path = '/sys/class/infiniband_verbs'
if os.path.exists(rdma_path):
print(f"[{rank}] RDMA verbs devices: {os.listdir(rdma_path)}")
else:
print(f"[{rank}] /sys/class/infiniband_verbs does NOT exist")
# STEP 4: Setup GPU FIRST (before process group init) # STEP 4: Setup GPU FIRST (before process group init)
device = torch.device("cuda:0") device = torch.device("cuda:0")
torch.cuda.set_device(device) torch.cuda.set_device(device)
@@ -231,9 +267,12 @@ def run_training_node_func(rank, world_size):
batch_size = 32 batch_size = 32
num_batches = 5 num_batches = 5
training_start = time.time()
for epoch in range(num_epochs): for epoch in range(num_epochs):
ddp_model.train() ddp_model.train()
epoch_loss = 0.0 epoch_loss = 0.0
epoch_start = time.time()
for batch_idx in range(num_batches): for batch_idx in range(num_batches):
torch.manual_seed(epoch * num_batches + batch_idx) torch.manual_seed(epoch * num_batches + batch_idx)
@@ -249,20 +288,34 @@ def run_training_node_func(rank, world_size):
epoch_loss += loss.item() epoch_loss += loss.item()
avg_loss = epoch_loss / num_batches avg_loss = epoch_loss / num_batches
epoch_time = time.time() - epoch_start
# Synchronize loss across ranks # Synchronize loss across ranks
loss_tensor = torch.tensor([avg_loss]).to(device) loss_tensor = torch.tensor([avg_loss]).to(device)
dist.all_reduce(loss_tensor, op=dist.ReduceOp.AVG) dist.all_reduce(loss_tensor, op=dist.ReduceOp.AVG)
global_avg_loss = loss_tensor.item() global_avg_loss = loss_tensor.item()
if rank == 0: print(f"[{rank}] Epoch {epoch+1}/{num_epochs} DONE | Local Avg: {avg_loss:.6f} | Global Avg: {global_avg_loss:.6f} | Time: {epoch_time:.3f}s")
print(f"[{rank}] Epoch {epoch+1}/{num_epochs} | Global Avg Loss: {global_avg_loss:.6f}")
total_time = time.time() - training_start
print(f"\n[{rank}] {'='*60}") print(f"\n[{rank}] {'='*60}")
print(f"[{rank}] Training Complete!") print(f"[{rank}] Training Complete!")
print(f"[{rank}] Total training time: {total_time:.3f}s")
print(f"[{rank}] {'='*60}") print(f"[{rank}] {'='*60}")
# STEP 8: Cleanup # Print NCCL transport info from log file
print(f"\n[{rank}] === NCCL Transport Summary ===")
try:
with open(nccl_log_file, 'r') as f:
nccl_lines = f.readlines()
for line in nccl_lines:
line = line.strip()
if any(kw in line for kw in ['NET/IB', 'GPU Direct', 'Channel', 'via NET', 'via Socket', 'transport']):
print(f"[{rank}] NCCL: {line}")
except Exception as e:
print(f"[{rank}] Could not read NCCL log: {e}")
dist.destroy_process_group() dist.destroy_process_group()
print(f"[{rank}] Process group destroyed. Finished at {datetime.now()}") print(f"[{rank}] Process group destroyed. Finished at {datetime.now()}")

View File

@@ -38,6 +38,18 @@ def run_training_node_func(task_index):
import socket import socket
from datetime import datetime from datetime import datetime
import time import time
import sys
import tempfile
# Set NCCL environment variables BEFORE importing TensorFlow
nccl_log_file = tempfile.mktemp(suffix='.nccl.log')
os.environ['NCCL_DEBUG'] = 'TRACE'
os.environ['NCCL_DEBUG_FILE'] = nccl_log_file
os.environ['NCCL_DEBUG_SUBSYS'] = 'NET'
os.environ['NCCL_IB_DISABLE'] = '0' # Enable InfiniBand
os.environ['NCCL_IB_GPU_DIRECT_RDMA_MAX_DISTANCE'] = '6'
os.environ['NCCL_P2P_DISABLE'] = '0'
os.environ['NCCL_IGNORE_CPU_AFFINITY'] = '1'
hostname = socket.gethostname() hostname = socket.gethostname()
my_address = f"{hostname}.airflow-worker-gpu:{WORKER_PORT}" my_address = f"{hostname}.airflow-worker-gpu:{WORKER_PORT}"
@@ -110,6 +122,30 @@ def run_training_node_func(task_index):
print(f" Cluster: {cluster_spec}") print(f" Cluster: {cluster_spec}")
print(f" Task type: worker") print(f" Task type: worker")
print(f" Task index: {task_index}") print(f" Task index: {task_index}")
print(f" NCCL_IB_DISABLE: {os.environ.get('NCCL_IB_DISABLE')} (IB enabled)")
# Pre-flight IB diagnostics
print(f"[Worker-{task_index}] Running IB pre-flight checks...")
import subprocess
ib_dev_path = '/dev/infiniband'
if os.path.exists(ib_dev_path):
print(f"[Worker-{task_index}] /dev/infiniband: {os.listdir(ib_dev_path)}")
else:
print(f"[Worker-{task_index}] /dev/infiniband does NOT exist")
ib_sys_path = '/sys/class/infiniband'
if os.path.exists(ib_sys_path):
ib_devs = os.listdir(ib_sys_path)
print(f"[Worker-{task_index}] /sys/class/infiniband: {ib_devs}")
for dev in ib_devs:
try:
result = subprocess.run(['ibv_devinfo', '-d', dev], capture_output=True, text=True, timeout=10)
for line in result.stdout.split('\n'):
if any(k in line for k in ['hca_id', 'state', 'link_layer', 'port_lid']):
print(f" {line.strip()}")
except Exception:
pass
else:
print(f"[Worker-{task_index}] /sys/class/infiniband does NOT exist")
# NOW import TensorFlow after TF_CONFIG is set # NOW import TensorFlow after TF_CONFIG is set
import tensorflow as tf import tensorflow as tf
@@ -214,9 +250,11 @@ def run_training_node_func(task_index):
return loss return loss
history_loss = [] history_loss = []
training_start = time.time()
for epoch in range(num_epochs): for epoch in range(num_epochs):
epoch_losses = [] epoch_losses = []
epoch_start = time.time()
# Get batches from dataset # Get batches from dataset
batch_iter = iter(dataset) batch_iter = iter(dataset)
@@ -232,14 +270,29 @@ def run_training_node_func(task_index):
avg_loss = sum(epoch_losses) / len(epoch_losses) avg_loss = sum(epoch_losses) / len(epoch_losses)
history_loss.append(avg_loss) history_loss.append(avg_loss)
epoch_time = time.time() - epoch_start
if task_index == 0: print(f"[Worker-{task_index}] Epoch {epoch+1}/{num_epochs} DONE | Loss: {avg_loss:.6f} | Time: {epoch_time:.3f}s")
print(f"[Worker-{task_index}] Epoch {epoch+1}/{num_epochs} | Loss: {avg_loss:.6f}")
total_time = time.time() - training_start
print(f"\n[Worker-{task_index}] {'='*60}") print(f"\n[Worker-{task_index}] {'='*60}")
print(f"[Worker-{task_index}] Training Complete!") print(f"[Worker-{task_index}] Training Complete!")
print(f"[Worker-{task_index}] Total training time: {total_time:.3f}s")
print(f"[Worker-{task_index}] {'='*60}") print(f"[Worker-{task_index}] {'='*60}")
# Print NCCL transport info from log file
print(f"\n[Worker-{task_index}] === NCCL Transport Summary ===")
try:
with open(nccl_log_file, 'r') as f:
nccl_lines = f.readlines()
for line in nccl_lines:
line = line.strip()
if any(kw in line for kw in ['NET/IB', 'GPU Direct', 'Channel', 'via NET', 'via Socket', 'transport']):
print(f"[Worker-{task_index}] NCCL: {line}")
except Exception as e:
print(f"[Worker-{task_index}] Could not read NCCL log: {e}")
# Get final metrics # Get final metrics
final_loss = history_loss[-1] final_loss = history_loss[-1]