oriented-det 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- export/__init__.py +9 -0
- export/ort_runtime.py +67 -0
- export/postprocess.py +151 -0
- export/scripts/__init__.py +6 -0
- export/scripts/build_faster_rcnn_savedmodel.py +104 -0
- export/scripts/export_onnx.py +210 -0
- export/scripts/onnx_to_savedmodel.py +35 -0
- export/scripts/predict_savedmodel.py +94 -0
- export/scripts/save_predictions_tf.py +447 -0
- export/scripts/to_tflite.py +32 -0
- export/tests/test_export_onnx_optional.py +82 -0
- export/tests/test_export_wrappers.py +105 -0
- export/tests/test_faster_rcnn_export_parity.py +201 -0
- export/tests/test_ort_runtime.py +41 -0
- export/tf_serving_model.py +96 -0
- export/val_dataset.py +116 -0
- export/wrappers.py +161 -0
- oriented_det/__init__.py +77 -0
- oriented_det/cli/__init__.py +92 -0
- oriented_det/cli/train.py +18 -0
- oriented_det/configs/_base_/augmentation.json +21 -0
- oriented_det/configs/_base_/datasets/dota_le90.json +21 -0
- oriented_det/configs/_base_/fp16.json +5 -0
- oriented_det/configs/_base_/models/oriented_rcnn_r50.json +26 -0
- oriented_det/configs/_base_/models/rotated_faster_rcnn_r50.json +53 -0
- oriented_det/configs/_base_/models/rotated_retinanet_r50.json +30 -0
- oriented_det/configs/_base_/preprocessing.json +8 -0
- oriented_det/configs/_base_/schedules/1x.json +33 -0
- oriented_det/configs/config.schema.json +404 -0
- oriented_det/configs/oriented_rcnn/dota_le90_1x.json +121 -0
- oriented_det/configs/oriented_rcnn/dota_le90_3x.json +11 -0
- oriented_det/configs/rotated_faster_rcnn/dota_le90_1x.json +119 -0
- oriented_det/configs/rotated_faster_rcnn/dota_le90_3x.json +9 -0
- oriented_det/configs/rotated_retinanet/dota_le90_1x.json +107 -0
- oriented_det/configs/rotated_retinanet/dota_le90_3x.json +30 -0
- oriented_det/data/__init__.py +89 -0
- oriented_det/data/airbus_playground.py +611 -0
- oriented_det/data/dota.py +741 -0
- oriented_det/data/dota_classes.py +26 -0
- oriented_det/data/evaluation.py +648 -0
- oriented_det/data/flips.py +115 -0
- oriented_det/data/preprocessing.py +335 -0
- oriented_det/data/tiling.py +399 -0
- oriented_det/data/transforms.py +377 -0
- oriented_det/geometry/__init__.py +8 -0
- oriented_det/geometry/poly.py +127 -0
- oriented_det/geometry/qbox.py +63 -0
- oriented_det/geometry/rbox.py +250 -0
- oriented_det/geometry/transforms.py +266 -0
- oriented_det/models/__init__.py +36 -0
- oriented_det/models/backbones/__init__.py +11 -0
- oriented_det/models/backbones/resnet_fpn.py +79 -0
- oriented_det/models/backbones/utils.py +81 -0
- oriented_det/models/bbox_coder.py +355 -0
- oriented_det/models/faster_rcnn_inference.py +494 -0
- oriented_det/models/horizontal_roi_coder.py +155 -0
- oriented_det/models/oriented_rcnn.py +1256 -0
- oriented_det/models/oriented_roi.py +1664 -0
- oriented_det/models/oriented_rpn.py +2104 -0
- oriented_det/models/rotated_retinanet.py +1030 -0
- oriented_det/models/utils.py +590 -0
- oriented_det/ops/__init__.py +58 -0
- oriented_det/ops/gpu_ops.py +1109 -0
- oriented_det/ops/iou.py +172 -0
- oriented_det/ops/kfiou.py +275 -0
- oriented_det/ops/nms.py +202 -0
- oriented_det/ops/probiou.py +165 -0
- oriented_det/ops/rotated_ops.py +122 -0
- oriented_det/ops/utils.py +257 -0
- oriented_det/pretrained/__init__.py +23 -0
- oriented_det/pretrained/hub.py +249 -0
- oriented_det/pretrained/manifest.json +46 -0
- oriented_det/runtime/__init__.py +29 -0
- oriented_det/runtime/checkpoint.py +274 -0
- oriented_det/runtime/collate.py +348 -0
- oriented_det/runtime/inference.py +1286 -0
- oriented_det/train/__init__.py +102 -0
- oriented_det/train/config.py +872 -0
- oriented_det/train/engine.py +2933 -0
- oriented_det/train/grouped_ce.py +139 -0
- oriented_det/train/piecewise_schedule.py +33 -0
- oriented_det/train/profiler.py +287 -0
- oriented_det/train/utils.py +999 -0
- oriented_det/utils/__init__.py +31 -0
- oriented_det/utils/config.py +376 -0
- oriented_det/utils/device.py +35 -0
- oriented_det/utils/logging.py +163 -0
- oriented_det/utils/progress.py +62 -0
- oriented_det/utils/viz.py +181 -0
- oriented_det-0.1.0.dist-info/METADATA +313 -0
- oriented_det-0.1.0.dist-info/RECORD +115 -0
- oriented_det-0.1.0.dist-info/WHEEL +5 -0
- oriented_det-0.1.0.dist-info/entry_points.txt +2 -0
- oriented_det-0.1.0.dist-info/licenses/LICENSE +202 -0
- oriented_det-0.1.0.dist-info/top_level.txt +3 -0
- tools/__init__.py +1 -0
- tools/app.py +1284 -0
- tools/dataset_stats.py +389 -0
- tools/dota_labels_to_comma.py +132 -0
- tools/free_gpu.py +142 -0
- tools/generate_airbus_playground_csv.py +154 -0
- tools/image_demo.py +200 -0
- tools/lr_finder.py +771 -0
- tools/measure_sampled_riou_error.py +483 -0
- tools/playground_to_dota.py +290 -0
- tools/pretrained_download.py +49 -0
- tools/preview_augmentation.py +510 -0
- tools/publish_checkpoint.py +96 -0
- tools/save_predictions.py +2350 -0
- tools/sync_vendored_configs.py +94 -0
- tools/tile_dota.py +447 -0
- tools/train.py +2324 -0
- tools/train_example.py +244 -0
- tools/train_multi_gpu.py +367 -0
- tools/visualize_boxes.py +183 -0
tools/train_example.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""Example: Training oriented detection models on DOTA dataset.
|
|
2
|
+
|
|
3
|
+
This script demonstrates how to:
|
|
4
|
+
1. Load DOTA dataset
|
|
5
|
+
2. Set up model and optimizer
|
|
6
|
+
3. Train with the training engine
|
|
7
|
+
4. Evaluate on validation set
|
|
8
|
+
5. Save checkpoints
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
# Set CUDA memory allocation configuration before importing PyTorch
|
|
13
|
+
# This helps with CUDA out of memory errors by using expandable segments
|
|
14
|
+
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
import torch
|
|
22
|
+
import torch.optim as optim
|
|
23
|
+
from torch.utils.data import DataLoader
|
|
24
|
+
except ImportError as e:
|
|
25
|
+
print(f"Required dependencies not installed: {e}")
|
|
26
|
+
print("Please install: pip install torch")
|
|
27
|
+
sys.exit(1)
|
|
28
|
+
|
|
29
|
+
from oriented_det import OrientedRCNN, RotatedRetinaNet
|
|
30
|
+
from oriented_det.data import build_dota_loader, DOTADataset
|
|
31
|
+
from oriented_det.train import train, CheckpointManager, MetricTracker
|
|
32
|
+
from oriented_det.train.utils import collate_dota_samples
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
from torch.utils.tensorboard import SummaryWriter
|
|
36
|
+
TENSORBOARD_AVAILABLE = True
|
|
37
|
+
except ImportError:
|
|
38
|
+
TENSORBOARD_AVAILABLE = False
|
|
39
|
+
SummaryWriter = None # type: ignore
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def create_model(model_type: str, num_classes: int, backbone: str = "resnet50", pretrained: bool = False):
|
|
43
|
+
"""Create a detection model.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
model_type: "oriented_rcnn" or "rotated_retinanet"
|
|
47
|
+
num_classes: Number of detection classes
|
|
48
|
+
backbone: Backbone name
|
|
49
|
+
pretrained: Whether to use pretrained backbone
|
|
50
|
+
"""
|
|
51
|
+
if model_type == "oriented_rcnn":
|
|
52
|
+
return OrientedRCNN(
|
|
53
|
+
num_classes=num_classes,
|
|
54
|
+
backbone_name=backbone,
|
|
55
|
+
pretrained_backbone=pretrained,
|
|
56
|
+
)
|
|
57
|
+
elif model_type == "rotated_retinanet":
|
|
58
|
+
return RotatedRetinaNet(
|
|
59
|
+
num_classes=num_classes,
|
|
60
|
+
backbone_name=backbone,
|
|
61
|
+
pretrained_backbone=pretrained,
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
raise ValueError(f"Unknown model type: {model_type}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def create_data_loaders(
|
|
68
|
+
data_root: Path,
|
|
69
|
+
batch_size: int = 4,
|
|
70
|
+
num_workers: int = 4,
|
|
71
|
+
allowed_classes: list = None,
|
|
72
|
+
difficult_strategy: str = "drop",
|
|
73
|
+
):
|
|
74
|
+
"""Create training and validation data loaders.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
data_root: Root directory of DOTA dataset
|
|
78
|
+
batch_size: Batch size
|
|
79
|
+
num_workers: Number of data loading workers
|
|
80
|
+
allowed_classes: Optional list of allowed class names
|
|
81
|
+
difficult_strategy: drop | ignore | keep (DOTA difficult flag handling)
|
|
82
|
+
"""
|
|
83
|
+
# Note: This is a simplified example. In practice, you'd need to:
|
|
84
|
+
# 1. Load images properly (DOTADataset returns paths, not tensors)
|
|
85
|
+
# 2. Apply transforms/augmentation
|
|
86
|
+
# 3. Create a proper collate function that loads images
|
|
87
|
+
|
|
88
|
+
train_loader = build_dota_loader(
|
|
89
|
+
root_dir=data_root,
|
|
90
|
+
split="train",
|
|
91
|
+
batch_size=batch_size,
|
|
92
|
+
shuffle=True,
|
|
93
|
+
num_workers=num_workers,
|
|
94
|
+
allowed_classes=allowed_classes,
|
|
95
|
+
difficult_strategy=difficult_strategy,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
val_loader = build_dota_loader(
|
|
99
|
+
root_dir=data_root,
|
|
100
|
+
split="val",
|
|
101
|
+
batch_size=batch_size,
|
|
102
|
+
shuffle=False,
|
|
103
|
+
num_workers=num_workers,
|
|
104
|
+
allowed_classes=allowed_classes,
|
|
105
|
+
difficult_strategy=difficult_strategy,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return train_loader, val_loader
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def main():
|
|
112
|
+
parser = argparse.ArgumentParser(description="Train oriented detection model on DOTA")
|
|
113
|
+
parser.add_argument("data_root", type=Path, help="Root directory of DOTA dataset")
|
|
114
|
+
parser.add_argument("--model-type", choices=["oriented_rcnn", "rotated_retinanet"], default="oriented_rcnn",
|
|
115
|
+
help="Model type to train")
|
|
116
|
+
parser.add_argument("--backbone", default="resnet50", help="Backbone architecture")
|
|
117
|
+
parser.add_argument("--num-classes", type=int, default=15, help="Number of classes")
|
|
118
|
+
parser.add_argument("--batch-size", type=int, default=4, help="Batch size")
|
|
119
|
+
parser.add_argument("--num-epochs", type=int, default=12, help="Number of training epochs")
|
|
120
|
+
parser.add_argument("--lr", type=float, default=0.001, help="Learning rate")
|
|
121
|
+
parser.add_argument("--device", default=None,
|
|
122
|
+
help="Device to train on")
|
|
123
|
+
parser.add_argument("--checkpoint-dir", type=Path, default=Path("checkpoints"),
|
|
124
|
+
help="Directory to save checkpoints")
|
|
125
|
+
parser.add_argument("--resume", type=Path, help="Path to checkpoint to resume from")
|
|
126
|
+
parser.add_argument("--use-amp", action="store_true", help="Use automatic mixed precision")
|
|
127
|
+
parser.add_argument("--gradient-accumulation", type=int, default=1,
|
|
128
|
+
help="Gradient accumulation steps")
|
|
129
|
+
parser.add_argument("--num-workers", type=int, default=4, help="Number of data loading workers")
|
|
130
|
+
parser.add_argument("--log-dir", type=Path, help="Directory for TensorBoard logs (enables TensorBoard if provided)")
|
|
131
|
+
|
|
132
|
+
args = parser.parse_args()
|
|
133
|
+
|
|
134
|
+
if args.device is None:
|
|
135
|
+
from oriented_det.utils import get_device
|
|
136
|
+
args.device = str(get_device())
|
|
137
|
+
|
|
138
|
+
# Create model
|
|
139
|
+
print(f"Creating {args.model_type} model with {args.num_classes} classes...")
|
|
140
|
+
model = create_model(args.model_type, args.num_classes, args.backbone, pretrained=True)
|
|
141
|
+
model.to(args.device)
|
|
142
|
+
|
|
143
|
+
# Create data loaders
|
|
144
|
+
print(f"Loading DOTA dataset from {args.data_root}...")
|
|
145
|
+
try:
|
|
146
|
+
train_loader, val_loader = create_data_loaders(
|
|
147
|
+
args.data_root,
|
|
148
|
+
batch_size=args.batch_size,
|
|
149
|
+
num_workers=args.num_workers,
|
|
150
|
+
)
|
|
151
|
+
print(f"Train samples: {len(train_loader.dataset)}")
|
|
152
|
+
print(f"Val samples: {len(val_loader.dataset)}")
|
|
153
|
+
except Exception as e:
|
|
154
|
+
print(f"Error loading dataset: {e}")
|
|
155
|
+
print("\nNote: This example requires a properly formatted DOTA dataset.")
|
|
156
|
+
print("The dataset should have the following structure:")
|
|
157
|
+
print(" data_root/")
|
|
158
|
+
print(" labelTxt/ (annotation files)")
|
|
159
|
+
print(" images/ (image files)")
|
|
160
|
+
sys.exit(1)
|
|
161
|
+
|
|
162
|
+
# Create optimizer
|
|
163
|
+
optimizer = optim.SGD(
|
|
164
|
+
model.parameters(),
|
|
165
|
+
lr=args.lr,
|
|
166
|
+
momentum=0.9,
|
|
167
|
+
weight_decay=0.0001,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Learning rate scheduler
|
|
171
|
+
lr_scheduler = optim.lr_scheduler.StepLR(
|
|
172
|
+
optimizer,
|
|
173
|
+
step_size=8,
|
|
174
|
+
gamma=0.1,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Checkpoint manager
|
|
178
|
+
checkpoint_manager = CheckpointManager(
|
|
179
|
+
args.checkpoint_dir,
|
|
180
|
+
best_metric="total_loss", # In practice, use mAP
|
|
181
|
+
higher_is_better=False,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Setup TensorBoard writer if log_dir is provided
|
|
185
|
+
writer = None
|
|
186
|
+
if args.log_dir:
|
|
187
|
+
if not TENSORBOARD_AVAILABLE:
|
|
188
|
+
print("Warning: TensorBoard is not available. Install with: pip install tensorboard")
|
|
189
|
+
else:
|
|
190
|
+
args.log_dir.mkdir(parents=True, exist_ok=True)
|
|
191
|
+
writer = SummaryWriter(log_dir=str(args.log_dir))
|
|
192
|
+
print(f"TensorBoard logging enabled. Logs saved to: {args.log_dir}")
|
|
193
|
+
print(f"View logs with: tensorboard --logdir {args.log_dir}")
|
|
194
|
+
|
|
195
|
+
# Resume from checkpoint if provided
|
|
196
|
+
start_epoch = 0
|
|
197
|
+
if args.resume and args.resume.exists():
|
|
198
|
+
print(f"Resuming from checkpoint: {args.resume}")
|
|
199
|
+
checkpoint_manager.load(args.resume, model, optimizer)
|
|
200
|
+
start_epoch = checkpoint_manager.load(args.resume, model, optimizer).get("epoch", 0) + 1
|
|
201
|
+
|
|
202
|
+
# Training configuration
|
|
203
|
+
print("\nTraining configuration:")
|
|
204
|
+
print(f" Model: {args.model_type}")
|
|
205
|
+
print(f" Backbone: {args.backbone}")
|
|
206
|
+
print(f" Batch size: {args.batch_size}")
|
|
207
|
+
print(f" Learning rate: {args.lr}")
|
|
208
|
+
print(f" Epochs: {args.num_epochs}")
|
|
209
|
+
print(f" Device: {args.device}")
|
|
210
|
+
print(f" Mixed precision: {args.use_amp}")
|
|
211
|
+
print(f" Gradient accumulation: {args.gradient_accumulation}")
|
|
212
|
+
print()
|
|
213
|
+
|
|
214
|
+
# Train
|
|
215
|
+
try:
|
|
216
|
+
history = train(
|
|
217
|
+
model=model,
|
|
218
|
+
train_loader=train_loader,
|
|
219
|
+
optimizer=optimizer,
|
|
220
|
+
device=torch.device(args.device),
|
|
221
|
+
num_epochs=args.num_epochs,
|
|
222
|
+
val_loader=val_loader,
|
|
223
|
+
lr_scheduler=lr_scheduler,
|
|
224
|
+
checkpoint_manager=checkpoint_manager,
|
|
225
|
+
use_amp=args.use_amp,
|
|
226
|
+
gradient_accumulation_steps=args.gradient_accumulation,
|
|
227
|
+
max_grad_norm=1.0,
|
|
228
|
+
start_epoch=start_epoch,
|
|
229
|
+
writer=writer,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
print("\nTraining completed!")
|
|
233
|
+
print(f"Checkpoints saved to: {args.checkpoint_dir}")
|
|
234
|
+
|
|
235
|
+
except KeyboardInterrupt:
|
|
236
|
+
print("\nTraining interrupted by user")
|
|
237
|
+
print(f"Checkpoints saved to: {args.checkpoint_dir}")
|
|
238
|
+
except Exception as e:
|
|
239
|
+
print(f"\nTraining error: {e}")
|
|
240
|
+
raise
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
if __name__ == "__main__":
|
|
244
|
+
main()
|
tools/train_multi_gpu.py
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Multi-GPU training wrapper for train.py.
|
|
3
|
+
|
|
4
|
+
This script launches train.py with torchrun for distributed training across multiple GPUs.
|
|
5
|
+
Configuration is read from JSON config files; pass --config <path> and other train.py arguments.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python train_multi_gpu.py [--nproc-per-node N] --config <config.json> [train.py arguments...]
|
|
9
|
+
|
|
10
|
+
Examples:
|
|
11
|
+
# Use all available GPUs with default config
|
|
12
|
+
python train_multi_gpu.py --config configs/rotated_faster_rcnn/dota_le90_3x.json
|
|
13
|
+
|
|
14
|
+
# Use 4 GPUs
|
|
15
|
+
python train_multi_gpu.py --nproc-per-node 4 --config configs/rotated_faster_rcnn/dota_le90_3x.json
|
|
16
|
+
|
|
17
|
+
# Use 2 GPUs with custom arguments
|
|
18
|
+
python train_multi_gpu.py --nproc-per-node 2 --config configs/.../config.json --use-amp
|
|
19
|
+
|
|
20
|
+
Checkpoint loading is configured only in the JSON ``checkpoint`` section (same as ``train.py``).
|
|
21
|
+
|
|
22
|
+
The script automatically:
|
|
23
|
+
- Detects the number of available GPUs
|
|
24
|
+
- Launches train.py with torchrun
|
|
25
|
+
- Forwards all arguments (including --config) to train.py
|
|
26
|
+
- Sets up proper environment variables for distributed training
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import subprocess
|
|
30
|
+
import sys
|
|
31
|
+
import os
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
import torch
|
|
36
|
+
except ImportError:
|
|
37
|
+
print("Error: PyTorch is not installed. Please install PyTorch first.")
|
|
38
|
+
sys.exit(1)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_num_gpus():
|
|
42
|
+
"""Get the number of available GPUs."""
|
|
43
|
+
if not torch.cuda.is_available():
|
|
44
|
+
print("Warning: CUDA is not available. Falling back to CPU training.")
|
|
45
|
+
return 0
|
|
46
|
+
return torch.cuda.device_count()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def find_torchrun():
|
|
50
|
+
"""Find torchrun executable."""
|
|
51
|
+
# Try torchrun first (PyTorch 1.9+)
|
|
52
|
+
try:
|
|
53
|
+
result = subprocess.run(
|
|
54
|
+
["torchrun", "--help"],
|
|
55
|
+
capture_output=True,
|
|
56
|
+
text=True,
|
|
57
|
+
timeout=5,
|
|
58
|
+
)
|
|
59
|
+
if result.returncode == 0:
|
|
60
|
+
return "torchrun"
|
|
61
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
# Fallback to python -m torch.distributed.launch
|
|
65
|
+
try:
|
|
66
|
+
result = subprocess.run(
|
|
67
|
+
[sys.executable, "-m", "torch.distributed.launch", "--help"],
|
|
68
|
+
capture_output=True,
|
|
69
|
+
text=True,
|
|
70
|
+
timeout=5,
|
|
71
|
+
)
|
|
72
|
+
if result.returncode == 0:
|
|
73
|
+
return "python -m torch.distributed.launch"
|
|
74
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def main():
|
|
81
|
+
"""Main entry point."""
|
|
82
|
+
import argparse
|
|
83
|
+
|
|
84
|
+
parser = argparse.ArgumentParser(
|
|
85
|
+
description="Multi-GPU training wrapper for train.py (config-based training)",
|
|
86
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
87
|
+
epilog="""
|
|
88
|
+
Examples:
|
|
89
|
+
# Use all available GPUs (pass --config and any train.py options)
|
|
90
|
+
python train_multi_gpu.py --config configs/rotated_faster_rcnn/dota_le90_3x.json
|
|
91
|
+
|
|
92
|
+
# Use 4 GPUs
|
|
93
|
+
python train_multi_gpu.py --nproc-per-node 4 --config configs/.../config.json
|
|
94
|
+
|
|
95
|
+
# Use 2 GPUs with custom arguments
|
|
96
|
+
python train_multi_gpu.py --nproc-per-node 2 --config configs/.../config.json --use-amp --debug
|
|
97
|
+
""",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
parser.add_argument(
|
|
101
|
+
"--nproc-per-node",
|
|
102
|
+
type=int,
|
|
103
|
+
default=None,
|
|
104
|
+
help="Number of processes per node (default: number of available GPUs)",
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
parser.add_argument(
|
|
108
|
+
"--master-port",
|
|
109
|
+
type=int,
|
|
110
|
+
default=29500,
|
|
111
|
+
help="Master port for distributed training (default: 29500)",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
parser.add_argument(
|
|
115
|
+
"--backend",
|
|
116
|
+
choices=("gloo", "nccl"),
|
|
117
|
+
default=None,
|
|
118
|
+
help="Distributed backend (default: nccl for single-node, faster than Gloo with P2P/SHM enabled)",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Parse known args to separate wrapper args from train.py args
|
|
122
|
+
args, unknown_args = parser.parse_known_args()
|
|
123
|
+
|
|
124
|
+
# Get number of GPUs
|
|
125
|
+
num_gpus = get_num_gpus()
|
|
126
|
+
|
|
127
|
+
if num_gpus == 0:
|
|
128
|
+
print("Error: No GPUs available. Cannot run multi-GPU training.")
|
|
129
|
+
print("Please use train.py directly for CPU or single-GPU training.")
|
|
130
|
+
sys.exit(1)
|
|
131
|
+
|
|
132
|
+
# Determine number of processes
|
|
133
|
+
nproc_per_node = args.nproc_per_node or num_gpus
|
|
134
|
+
|
|
135
|
+
if nproc_per_node > num_gpus:
|
|
136
|
+
print(f"Warning: Requested {nproc_per_node} GPUs but only {num_gpus} available.")
|
|
137
|
+
print(f"Using {num_gpus} GPUs instead.")
|
|
138
|
+
nproc_per_node = num_gpus
|
|
139
|
+
|
|
140
|
+
if nproc_per_node < 1:
|
|
141
|
+
print("Error: nproc-per-node must be at least 1.")
|
|
142
|
+
sys.exit(1)
|
|
143
|
+
|
|
144
|
+
# Find torchrun
|
|
145
|
+
torchrun_cmd = find_torchrun()
|
|
146
|
+
if torchrun_cmd is None:
|
|
147
|
+
print("Error: Could not find torchrun or torch.distributed.launch.")
|
|
148
|
+
print("Please ensure PyTorch is properly installed.")
|
|
149
|
+
sys.exit(1)
|
|
150
|
+
|
|
151
|
+
# Train via installable module (odet train / oriented_det.cli.train)
|
|
152
|
+
train_module = "oriented_det.cli.train"
|
|
153
|
+
extra_args = unknown_args
|
|
154
|
+
if torchrun_cmd == "torchrun":
|
|
155
|
+
cmd = [
|
|
156
|
+
"torchrun",
|
|
157
|
+
"--nproc-per-node", str(nproc_per_node),
|
|
158
|
+
"--master-port", str(args.master_port),
|
|
159
|
+
"-m", train_module,
|
|
160
|
+
] + extra_args
|
|
161
|
+
else:
|
|
162
|
+
cmd = [
|
|
163
|
+
sys.executable,
|
|
164
|
+
"-m", "torch.distributed.launch",
|
|
165
|
+
"--nproc-per-node", str(nproc_per_node),
|
|
166
|
+
"--master-port", str(args.master_port),
|
|
167
|
+
train_module,
|
|
168
|
+
] + extra_args
|
|
169
|
+
|
|
170
|
+
# Set up environment for single-node multi-GPU training
|
|
171
|
+
env = os.environ.copy()
|
|
172
|
+
|
|
173
|
+
# Short TMPDIR to avoid Gloo "File name too long" (store paths can embed hostname)
|
|
174
|
+
if "TMPDIR" not in env:
|
|
175
|
+
for d in ("/dev/shm", "/tmp"):
|
|
176
|
+
if os.path.isdir(d):
|
|
177
|
+
env["TMPDIR"] = d
|
|
178
|
+
break
|
|
179
|
+
|
|
180
|
+
# Backend: default NCCL for all (faster with P2P/SHM enabled on single-node A100)
|
|
181
|
+
# With PyTorch 2.3+ and proper configuration, NCCL works reliably on single-node
|
|
182
|
+
if args.backend:
|
|
183
|
+
env["DIST_BACKEND"] = args.backend
|
|
184
|
+
else:
|
|
185
|
+
env.setdefault("DIST_BACKEND", "nccl") # NCCL is faster than Gloo on single-node multi-GPU
|
|
186
|
+
|
|
187
|
+
# Ensure MASTER_ADDR and MASTER_PORT are set for distributed init
|
|
188
|
+
env.setdefault("MASTER_ADDR", "127.0.0.1")
|
|
189
|
+
env.setdefault("MASTER_PORT", str(args.master_port))
|
|
190
|
+
|
|
191
|
+
socket_ifname = None
|
|
192
|
+
use_gib = False
|
|
193
|
+
|
|
194
|
+
# Configure NCCL for GCP. gIB is only available on A3 Ultra, A4, A4X instances.
|
|
195
|
+
# A2 instances (A100) do NOT support gIB - use socket-only NCCL (NVLink intra-node, sockets/gVNIC inter-node).
|
|
196
|
+
if env.get("DIST_BACKEND", "nccl") == "nccl":
|
|
197
|
+
# Check instance type to determine if gIB is available
|
|
198
|
+
try:
|
|
199
|
+
instance_type = subprocess.run(
|
|
200
|
+
["curl", "-s", "-H", "Metadata-Flavor: Google",
|
|
201
|
+
"http://metadata.google.internal/computeMetadata/v1/instance/machine-type"],
|
|
202
|
+
capture_output=True, text=True, timeout=2
|
|
203
|
+
)
|
|
204
|
+
if instance_type.returncode == 0:
|
|
205
|
+
instance_type = instance_type.stdout.strip().split("/")[-1]
|
|
206
|
+
# gIB is only available on A3 Ultra, A4, A4X (not A2)
|
|
207
|
+
if instance_type.startswith(("a3-ultra", "a4", "a4x")):
|
|
208
|
+
gib_script = "/usr/local/gib/scripts/set_nccl_env.sh"
|
|
209
|
+
if os.path.exists(gib_script):
|
|
210
|
+
use_gib = True
|
|
211
|
+
env["NCCL_NET"] = "gIB"
|
|
212
|
+
env["NCCL_CROSS_NIC"] = "0"
|
|
213
|
+
env["NCCL_NET_GDR_LEVEL"] = "PIX"
|
|
214
|
+
env["NCCL_P2P_NET_CHUNKSIZE"] = "131072"
|
|
215
|
+
env["NCCL_NVLS_CHUNKSIZE"] = "524288"
|
|
216
|
+
env["NCCL_IB_ADAPTIVE_ROUTING"] = "1"
|
|
217
|
+
env["NCCL_IB_QPS_PER_CONNECTION"] = "4"
|
|
218
|
+
env["NCCL_IB_TC"] = "52"
|
|
219
|
+
env["NCCL_IB_FIFO_TC"] = "84"
|
|
220
|
+
env["NCCL_TUNER_CONFIG_PATH"] = "/usr/local/gib/configs/tuner_config_a3u.txtpb"
|
|
221
|
+
print(f"Using GCP gIB backend (instance: {instance_type})")
|
|
222
|
+
else:
|
|
223
|
+
print(f"Warning: gIB script not found on {instance_type}, using socket-only NCCL")
|
|
224
|
+
else:
|
|
225
|
+
print(f"Instance {instance_type} does not support gIB (A2/A100), using socket-only NCCL")
|
|
226
|
+
else:
|
|
227
|
+
print("Could not detect instance type, using socket-only NCCL")
|
|
228
|
+
except Exception:
|
|
229
|
+
print("Could not detect instance type, using socket-only NCCL")
|
|
230
|
+
|
|
231
|
+
# A2 / socket-only: recommended NCCL settings (GDR off, socket tuning, NCCL 2.12.7+)
|
|
232
|
+
if not use_gib:
|
|
233
|
+
env.pop("NCCL_NET", None) # Ensure no inherited gIB on A2
|
|
234
|
+
env["NCCL_NET_GDR_LEVEL"] = "0" # Disable GPUDirect RDMA (unsupported on A2)
|
|
235
|
+
env["NCCL_SOCKET_NTHREADS"] = "4"
|
|
236
|
+
env["NCCL_NSOCKETS_PER_PEER"] = "4"
|
|
237
|
+
env.setdefault("NCCL_DEBUG", "WARN") # WARN: minimal output; use INFO for debugging
|
|
238
|
+
|
|
239
|
+
# Remove GCP gIB shim from LD_LIBRARY_PATH on A2 (shim enforces PIX, incompatible with A2)
|
|
240
|
+
# The shim (libnccl-net_internal.so) checks guest_config.txtpb and enforces gIB settings
|
|
241
|
+
ld_path = env.get("LD_LIBRARY_PATH", "")
|
|
242
|
+
if ld_path:
|
|
243
|
+
# Remove /usr/local/gib/lib64 to prevent shim from loading
|
|
244
|
+
paths = [p for p in ld_path.split(":") if p and not p.rstrip("/").endswith("/usr/local/gib/lib64")]
|
|
245
|
+
if paths:
|
|
246
|
+
env["LD_LIBRARY_PATH"] = ":".join(paths)
|
|
247
|
+
else:
|
|
248
|
+
env.pop("LD_LIBRARY_PATH", None)
|
|
249
|
+
print("Removed GCP gIB shim from LD_LIBRARY_PATH (A2 uses socket-only NCCL)")
|
|
250
|
+
|
|
251
|
+
# Detect and set network interface for single-node communication (required for socket-only NCCL)
|
|
252
|
+
socket_ifname = env.get("NCCL_SOCKET_IFNAME")
|
|
253
|
+
if not socket_ifname:
|
|
254
|
+
try:
|
|
255
|
+
result = subprocess.run(["ip", "route", "get", "8.8.8.8"], capture_output=True, text=True, timeout=2)
|
|
256
|
+
if result.returncode == 0:
|
|
257
|
+
for line in result.stdout.split('\n'):
|
|
258
|
+
if 'dev' in line:
|
|
259
|
+
parts = line.split()
|
|
260
|
+
if 'dev' in parts:
|
|
261
|
+
idx = parts.index('dev')
|
|
262
|
+
if idx + 1 < len(parts):
|
|
263
|
+
socket_ifname = parts[idx + 1]
|
|
264
|
+
break
|
|
265
|
+
except (FileNotFoundError, subprocess.TimeoutExpired, Exception):
|
|
266
|
+
pass
|
|
267
|
+
if not socket_ifname:
|
|
268
|
+
for ifname in ["ens8", "ens4", "eth0", "eno1"]:
|
|
269
|
+
try:
|
|
270
|
+
with open(f"/sys/class/net/{ifname}/operstate", "r") as f:
|
|
271
|
+
if "up" in f.read().lower():
|
|
272
|
+
socket_ifname = ifname
|
|
273
|
+
break
|
|
274
|
+
except (FileNotFoundError, IOError):
|
|
275
|
+
continue
|
|
276
|
+
if socket_ifname:
|
|
277
|
+
env["NCCL_SOCKET_IFNAME"] = socket_ifname
|
|
278
|
+
|
|
279
|
+
# For Gloo backend: set GLOO_SOCKET_IFNAME (reuse socket_ifname if already detected for NCCL)
|
|
280
|
+
if env.get("DIST_BACKEND", "nccl") == "gloo":
|
|
281
|
+
if not socket_ifname:
|
|
282
|
+
try:
|
|
283
|
+
result = subprocess.run(["ip", "route", "get", "8.8.8.8"], capture_output=True, text=True, timeout=2)
|
|
284
|
+
if result.returncode == 0:
|
|
285
|
+
for line in result.stdout.split('\n'):
|
|
286
|
+
if 'dev' in line:
|
|
287
|
+
parts = line.split()
|
|
288
|
+
if 'dev' in parts:
|
|
289
|
+
idx = parts.index('dev')
|
|
290
|
+
if idx + 1 < len(parts):
|
|
291
|
+
socket_ifname = parts[idx + 1]
|
|
292
|
+
break
|
|
293
|
+
except (FileNotFoundError, subprocess.TimeoutExpired, Exception):
|
|
294
|
+
pass
|
|
295
|
+
if not socket_ifname:
|
|
296
|
+
for ifname in ["ens8", "ens4", "eth0", "eno1"]:
|
|
297
|
+
try:
|
|
298
|
+
with open(f"/sys/class/net/{ifname}/operstate", "r") as f:
|
|
299
|
+
if "up" in f.read().lower():
|
|
300
|
+
socket_ifname = ifname
|
|
301
|
+
break
|
|
302
|
+
except (FileNotFoundError, IOError):
|
|
303
|
+
continue
|
|
304
|
+
|
|
305
|
+
# Optimize Gloo backend settings for better performance
|
|
306
|
+
if env.get("DIST_BACKEND", "nccl") == "gloo":
|
|
307
|
+
# Use proper network interface for Gloo
|
|
308
|
+
if socket_ifname:
|
|
309
|
+
env.setdefault("GLOO_SOCKET_IFNAME", socket_ifname)
|
|
310
|
+
# Increase Gloo timeout (default 30s, increase for large models)
|
|
311
|
+
env.setdefault("GLOO_TIMEOUT_SECONDS", "1800") # 30 minutes
|
|
312
|
+
# Use async operations where possible (if supported)
|
|
313
|
+
# Note: GLOO_ASYNC may not be available in all PyTorch versions
|
|
314
|
+
|
|
315
|
+
# Print info
|
|
316
|
+
print("=" * 80)
|
|
317
|
+
print("Multi-GPU Training Launcher")
|
|
318
|
+
print("=" * 80)
|
|
319
|
+
print(f"GPUs available: {num_gpus}")
|
|
320
|
+
print(f"GPUs to use: {nproc_per_node}")
|
|
321
|
+
print(f"Module: {train_module}")
|
|
322
|
+
proj = env.get("ORIENTED_DET_PROJECT_ROOT")
|
|
323
|
+
if proj:
|
|
324
|
+
print(f"ORIENTED_DET_PROJECT_ROOT: {proj}")
|
|
325
|
+
from oriented_det.pretrained import get_pretrained_dir
|
|
326
|
+
|
|
327
|
+
pre = env.get("ORIENTED_DET_PRETRAINED_DIR")
|
|
328
|
+
print(f"Pretrained cache: {pre or get_pretrained_dir()}")
|
|
329
|
+
print(f"Command: {' '.join(cmd)}")
|
|
330
|
+
print("=" * 80)
|
|
331
|
+
print("Distributed Training Configuration:")
|
|
332
|
+
backend = env.get('DIST_BACKEND', 'nccl')
|
|
333
|
+
print(f" Backend: {backend} (default: NCCL, use --backend gloo for Gloo)")
|
|
334
|
+
print(f" MASTER_ADDR: {env.get('MASTER_ADDR', '127.0.0.1')}")
|
|
335
|
+
print(f" MASTER_PORT: {env.get('MASTER_PORT', '29500')}")
|
|
336
|
+
if backend == "nccl":
|
|
337
|
+
net = env.get("NCCL_NET", "socket (default)")
|
|
338
|
+
ifname = env.get("NCCL_SOCKET_IFNAME", "not set")
|
|
339
|
+
if net == "gIB":
|
|
340
|
+
gdr_level = env.get("NCCL_NET_GDR_LEVEL", "not set")
|
|
341
|
+
print(f" NCCL_NET: {net} (gIB backend)")
|
|
342
|
+
print(f" NCCL_NET_GDR_LEVEL: {gdr_level}")
|
|
343
|
+
else:
|
|
344
|
+
print(f" NCCL_NET: {net} (socket-only, A2)")
|
|
345
|
+
print(f" NCCL_NET_GDR_LEVEL: {env.get('NCCL_NET_GDR_LEVEL', 'not set')}")
|
|
346
|
+
print(f" NCCL_SOCKET_NTHREADS: {env.get('NCCL_SOCKET_NTHREADS', 'not set')}")
|
|
347
|
+
print(f" NCCL_NSOCKETS_PER_PEER: {env.get('NCCL_NSOCKETS_PER_PEER', 'not set')}")
|
|
348
|
+
print(f" NCCL_DEBUG: {env.get('NCCL_DEBUG', 'not set')}")
|
|
349
|
+
print(f" NCCL_SOCKET_IFNAME: {ifname}")
|
|
350
|
+
print("=" * 80)
|
|
351
|
+
print()
|
|
352
|
+
|
|
353
|
+
# Launch training
|
|
354
|
+
try:
|
|
355
|
+
# Run the command with environment variables and forward stdout/stderr
|
|
356
|
+
result = subprocess.run(cmd, env=env, check=False)
|
|
357
|
+
sys.exit(result.returncode)
|
|
358
|
+
except KeyboardInterrupt:
|
|
359
|
+
print("\nTraining interrupted by user.")
|
|
360
|
+
sys.exit(130)
|
|
361
|
+
except Exception as e:
|
|
362
|
+
print(f"Error launching training: {e}")
|
|
363
|
+
sys.exit(1)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
if __name__ == "__main__":
|
|
367
|
+
main()
|