ultralytics 8.3.124__py3-none-any.whl → 8.3.126__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.
- tests/test_cuda.py +71 -66
- tests/test_python.py +5 -8
- ultralytics/__init__.py +1 -1
- ultralytics/cfg/__init__.py +4 -5
- ultralytics/cfg/default.yaml +3 -3
- ultralytics/data/utils.py +7 -7
- ultralytics/engine/exporter.py +7 -7
- ultralytics/engine/model.py +3 -3
- ultralytics/engine/trainer.py +3 -2
- ultralytics/engine/tuner.py +3 -3
- ultralytics/hub/session.py +1 -1
- ultralytics/models/yolo/model.py +3 -3
- ultralytics/nn/autobackend.py +4 -4
- ultralytics/nn/tasks.py +2 -2
- ultralytics/solutions/analytics.py +7 -6
- ultralytics/trackers/track.py +2 -2
- ultralytics/utils/__init__.py +118 -56
- ultralytics/utils/autodevice.py +175 -0
- ultralytics/utils/benchmarks.py +6 -10
- ultralytics/utils/checks.py +4 -4
- ultralytics/utils/metrics.py +6 -2
- ultralytics/utils/plotting.py +11 -5
- ultralytics/utils/torch_utils.py +18 -5
- {ultralytics-8.3.124.dist-info → ultralytics-8.3.126.dist-info}/METADATA +1 -1
- {ultralytics-8.3.124.dist-info → ultralytics-8.3.126.dist-info}/RECORD +29 -28
- {ultralytics-8.3.124.dist-info → ultralytics-8.3.126.dist-info}/WHEEL +1 -1
- {ultralytics-8.3.124.dist-info → ultralytics-8.3.126.dist-info}/entry_points.txt +0 -0
- {ultralytics-8.3.124.dist-info → ultralytics-8.3.126.dist-info}/licenses/LICENSE +0 -0
- {ultralytics-8.3.124.dist-info → ultralytics-8.3.126.dist-info}/top_level.txt +0 -0
@@ -3,10 +3,7 @@
|
|
3
3
|
from itertools import cycle
|
4
4
|
|
5
5
|
import cv2
|
6
|
-
import matplotlib.pyplot as plt
|
7
6
|
import numpy as np
|
8
|
-
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
|
9
|
-
from matplotlib.figure import Figure
|
10
7
|
|
11
8
|
from ultralytics.solutions.solutions import BaseSolution, SolutionResults # Import a parent class
|
12
9
|
|
@@ -32,7 +29,7 @@ class Analytics(BaseSolution):
|
|
32
29
|
clswise_count (Dict[str, int]): Dictionary for class-wise object counts.
|
33
30
|
fig (Figure): Matplotlib figure object for the chart.
|
34
31
|
ax (Axes): Matplotlib axes object for the chart.
|
35
|
-
canvas (
|
32
|
+
canvas (FigureCanvasAgg): Canvas for rendering the chart.
|
36
33
|
lines (dict): Dictionary to store line objects for area charts.
|
37
34
|
color_mapping (Dict[str, str]): Dictionary mapping class labels to colors for consistent visualization.
|
38
35
|
|
@@ -51,6 +48,10 @@ class Analytics(BaseSolution):
|
|
51
48
|
"""Initialize Analytics class with various chart types for visual data representation."""
|
52
49
|
super().__init__(**kwargs)
|
53
50
|
|
51
|
+
import matplotlib.pyplot as plt # scope for faster 'import ultralytics'
|
52
|
+
from matplotlib.backends.backend_agg import FigureCanvasAgg
|
53
|
+
from matplotlib.figure import Figure
|
54
|
+
|
54
55
|
self.type = self.CFG["analytics_type"] # type of analytics i.e "line", "pie", "bar" or "area" charts.
|
55
56
|
self.x_label = "Classes" if self.type in {"bar", "pie"} else "Frame#"
|
56
57
|
self.y_label = "Total Counts"
|
@@ -71,14 +72,14 @@ class Analytics(BaseSolution):
|
|
71
72
|
if self.type in {"line", "area"}:
|
72
73
|
self.lines = {}
|
73
74
|
self.fig = Figure(facecolor=self.bg_color, figsize=figsize)
|
74
|
-
self.canvas =
|
75
|
+
self.canvas = FigureCanvasAgg(self.fig) # Set common axis properties
|
75
76
|
self.ax = self.fig.add_subplot(111, facecolor=self.bg_color)
|
76
77
|
if self.type == "line":
|
77
78
|
(self.line,) = self.ax.plot([], [], color="cyan", linewidth=self.line_width)
|
78
79
|
elif self.type in {"bar", "pie"}:
|
79
80
|
# Initialize bar or pie plot
|
80
81
|
self.fig, self.ax = plt.subplots(figsize=figsize, facecolor=self.bg_color)
|
81
|
-
self.canvas =
|
82
|
+
self.canvas = FigureCanvasAgg(self.fig) # Set common axis properties
|
82
83
|
self.ax.set_facecolor(self.bg_color)
|
83
84
|
self.color_mapping = {}
|
84
85
|
|
ultralytics/trackers/track.py
CHANGED
@@ -5,7 +5,7 @@ from pathlib import Path
|
|
5
5
|
|
6
6
|
import torch
|
7
7
|
|
8
|
-
from ultralytics.utils import
|
8
|
+
from ultralytics.utils import YAML, IterableSimpleNamespace
|
9
9
|
from ultralytics.utils.checks import check_yaml
|
10
10
|
|
11
11
|
from .bot_sort import BOTSORT
|
@@ -39,7 +39,7 @@ def on_predict_start(predictor: object, persist: bool = False) -> None:
|
|
39
39
|
return
|
40
40
|
|
41
41
|
tracker = check_yaml(predictor.args.tracker)
|
42
|
-
cfg = IterableSimpleNamespace(**
|
42
|
+
cfg = IterableSimpleNamespace(**YAML.load(tracker))
|
43
43
|
|
44
44
|
if cfg.tracker_type not in {"bytetrack", "botsort"}:
|
45
45
|
raise AssertionError(f"Only 'bytetrack' and 'botsort' are supported for now, but got '{cfg.tracker_type}'")
|
ultralytics/utils/__init__.py
CHANGED
@@ -20,11 +20,9 @@ from typing import Union
|
|
20
20
|
from urllib.parse import unquote
|
21
21
|
|
22
22
|
import cv2
|
23
|
-
import matplotlib.pyplot as plt
|
24
23
|
import numpy as np
|
25
24
|
import torch
|
26
25
|
import tqdm
|
27
|
-
import yaml
|
28
26
|
|
29
27
|
from ultralytics import __version__
|
30
28
|
from ultralytics.utils.patches import imread, imshow, imwrite, torch_load, torch_save # for patches
|
@@ -184,6 +182,10 @@ class TQDM(rich.tqdm if TQDM_RICH else tqdm.tqdm):
|
|
184
182
|
kwargs.setdefault("bar_format", TQDM_BAR_FORMAT) # override default value if passed
|
185
183
|
super().__init__(*args, **kwargs)
|
186
184
|
|
185
|
+
def __iter__(self):
|
186
|
+
"""Return self as iterator to satisfy Iterable interface."""
|
187
|
+
return super().__iter__()
|
188
|
+
|
187
189
|
|
188
190
|
class SimpleClass:
|
189
191
|
"""
|
@@ -330,6 +332,8 @@ def plt_settings(rcparams=None, backend="Agg"):
|
|
330
332
|
|
331
333
|
def wrapper(*args, **kwargs):
|
332
334
|
"""Sets rc parameters and backend, calls the original function, and restores the settings."""
|
335
|
+
import matplotlib.pyplot as plt # scope for faster 'import ultralytics'
|
336
|
+
|
333
337
|
original_backend = plt.get_backend()
|
334
338
|
switch = backend.lower() != original_backend.lower()
|
335
339
|
if switch:
|
@@ -468,84 +472,142 @@ class ThreadingLocked:
|
|
468
472
|
return decorated
|
469
473
|
|
470
474
|
|
471
|
-
|
475
|
+
class YAML:
|
472
476
|
"""
|
473
|
-
|
477
|
+
YAML utility class for efficient file operations with automatic C-implementation detection.
|
474
478
|
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
+
This class provides optimized YAML loading and saving operations using PyYAML's fastest available implementation
|
480
|
+
(C-based when possible). It implements a singleton pattern with lazy initialization, allowing direct class method
|
481
|
+
usage without explicit instantiation. The class handles file path creation, validation, and character encoding
|
482
|
+
issues automatically.
|
479
483
|
|
480
|
-
|
481
|
-
|
484
|
+
The implementation prioritizes performance through:
|
485
|
+
- Automatic C-based loader/dumper selection when available
|
486
|
+
- Singleton pattern to reuse the same instance
|
487
|
+
- Lazy initialization to defer import costs until needed
|
488
|
+
- Fallback mechanisms for handling problematic YAML content
|
489
|
+
|
490
|
+
Attributes:
|
491
|
+
_instance: Internal singleton instance storage.
|
492
|
+
yaml: Reference to the PyYAML module.
|
493
|
+
SafeLoader: Best available YAML loader (CSafeLoader if available).
|
494
|
+
SafeDumper: Best available YAML dumper (CSafeDumper if available).
|
495
|
+
|
496
|
+
Examples:
|
497
|
+
>>> data = YAML.load("config.yaml")
|
498
|
+
>>> data["new_value"] = 123
|
499
|
+
>>> YAML.save("updated_config.yaml", data)
|
500
|
+
>>> YAML.print(data)
|
482
501
|
"""
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
502
|
+
|
503
|
+
_instance = None
|
504
|
+
|
505
|
+
@classmethod
|
506
|
+
def _get_instance(cls):
|
507
|
+
"""Initialize singleton instance on first use."""
|
508
|
+
if cls._instance is None:
|
509
|
+
cls._instance = cls()
|
510
|
+
return cls._instance
|
511
|
+
|
512
|
+
def __init__(self):
|
513
|
+
"""Initialize with optimal YAML implementation (C-based when available)."""
|
514
|
+
import yaml
|
515
|
+
|
516
|
+
self.yaml = yaml
|
517
|
+
# Use C-based implementation if available for better performance
|
518
|
+
try:
|
519
|
+
self.SafeLoader = yaml.CSafeLoader
|
520
|
+
self.SafeDumper = yaml.CSafeDumper
|
521
|
+
except (AttributeError, ImportError):
|
522
|
+
self.SafeLoader = yaml.SafeLoader
|
523
|
+
self.SafeDumper = yaml.SafeDumper
|
524
|
+
|
525
|
+
@classmethod
|
526
|
+
def save(cls, file="data.yaml", data=None, header=""):
|
527
|
+
"""
|
528
|
+
Save Python object as YAML file.
|
529
|
+
|
530
|
+
Args:
|
531
|
+
file (str | Path): Path to save YAML file.
|
532
|
+
data (dict | None): Dict or compatible object to save.
|
533
|
+
header (str): Optional string to add at file beginning.
|
534
|
+
"""
|
535
|
+
instance = cls._get_instance()
|
536
|
+
if data is None:
|
537
|
+
data = {}
|
538
|
+
|
539
|
+
# Create parent directories if needed
|
540
|
+
file = Path(file)
|
488
541
|
file.parent.mkdir(parents=True, exist_ok=True)
|
489
542
|
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
543
|
+
# Convert non-serializable objects to strings
|
544
|
+
valid_types = int, float, str, bool, list, tuple, dict, type(None)
|
545
|
+
for k, v in data.items():
|
546
|
+
if not isinstance(v, valid_types):
|
547
|
+
data[k] = str(v)
|
495
548
|
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
549
|
+
# Write YAML file
|
550
|
+
with open(file, "w", errors="ignore", encoding="utf-8") as f:
|
551
|
+
if header:
|
552
|
+
f.write(header)
|
553
|
+
instance.yaml.dump(data, f, sort_keys=False, allow_unicode=True, Dumper=instance.SafeDumper)
|
501
554
|
|
555
|
+
@classmethod
|
556
|
+
def load(cls, file="data.yaml", append_filename=False):
|
557
|
+
"""
|
558
|
+
Load YAML file to Python object with robust error handling.
|
502
559
|
|
503
|
-
|
504
|
-
|
505
|
-
|
560
|
+
Args:
|
561
|
+
file (str | Path): Path to YAML file.
|
562
|
+
append_filename (bool): Whether to add filename to returned dict.
|
506
563
|
|
507
|
-
|
508
|
-
|
509
|
-
|
564
|
+
Returns:
|
565
|
+
(dict): Loaded YAML content.
|
566
|
+
"""
|
567
|
+
instance = cls._get_instance()
|
568
|
+
assert str(file).endswith((".yaml", ".yml")), f"Not a YAML file: {file}"
|
510
569
|
|
511
|
-
|
512
|
-
(
|
513
|
-
|
514
|
-
assert Path(file).suffix in {".yaml", ".yml"}, f"Attempting to load non-YAML file {file} with yaml_load()"
|
515
|
-
with open(file, errors="ignore", encoding="utf-8") as f:
|
516
|
-
s = f.read() # string
|
570
|
+
# Read file content
|
571
|
+
with open(file, errors="ignore", encoding="utf-8") as f:
|
572
|
+
s = f.read()
|
517
573
|
|
518
|
-
#
|
519
|
-
|
574
|
+
# Try loading YAML with fallback for problematic characters
|
575
|
+
try:
|
576
|
+
data = instance.yaml.load(s, Loader=instance.SafeLoader) or {}
|
577
|
+
except Exception:
|
578
|
+
# Remove problematic characters and retry
|
520
579
|
s = re.sub(r"[^\x09\x0A\x0D\x20-\x7E\x85\xA0-\uD7FF\uE000-\uFFFD\U00010000-\U0010ffff]+", "", s)
|
580
|
+
data = instance.yaml.load(s, Loader=instance.SafeLoader) or {}
|
581
|
+
|
582
|
+
# Check for accidental user-error None strings (should be 'null' in YAML)
|
583
|
+
if "None" in data.values():
|
584
|
+
data = {k: None if v == "None" else v for k, v in data.items()}
|
521
585
|
|
522
|
-
# Add YAML filename to dict and return
|
523
|
-
data = yaml.safe_load(s) or {} # always return a dict (yaml.safe_load() may return None for empty files)
|
524
586
|
if append_filename:
|
525
587
|
data["yaml_file"] = str(file)
|
526
588
|
return data
|
527
589
|
|
590
|
+
@classmethod
|
591
|
+
def print(cls, yaml_file):
|
592
|
+
"""
|
593
|
+
Pretty print YAML file or object to console.
|
528
594
|
|
529
|
-
|
530
|
-
|
531
|
-
|
595
|
+
Args:
|
596
|
+
yaml_file (str | Path | dict): Path to YAML file or dict to print.
|
597
|
+
"""
|
598
|
+
instance = cls._get_instance()
|
532
599
|
|
533
|
-
|
534
|
-
|
600
|
+
# Load file if path provided
|
601
|
+
yaml_dict = cls.load(yaml_file) if isinstance(yaml_file, (str, Path)) else yaml_file
|
535
602
|
|
536
|
-
|
537
|
-
(
|
538
|
-
|
539
|
-
|
540
|
-
dump = yaml.dump(yaml_dict, sort_keys=False, allow_unicode=True, width=float("inf"))
|
541
|
-
LOGGER.info(f"Printing '{colorstr('bold', 'black', yaml_file)}'\n\n{dump}")
|
603
|
+
# Use -1 for unlimited width in C implementation
|
604
|
+
dump = instance.yaml.dump(yaml_dict, sort_keys=False, allow_unicode=True, width=-1, Dumper=instance.SafeDumper)
|
605
|
+
|
606
|
+
LOGGER.info(f"Printing '{colorstr('bold', 'black', yaml_file)}'\n\n{dump}")
|
542
607
|
|
543
608
|
|
544
609
|
# Default configuration
|
545
|
-
DEFAULT_CFG_DICT =
|
546
|
-
for k, v in DEFAULT_CFG_DICT.items():
|
547
|
-
if isinstance(v, str) and v.lower() == "none":
|
548
|
-
DEFAULT_CFG_DICT[k] = None
|
610
|
+
DEFAULT_CFG_DICT = YAML.load(DEFAULT_CFG_PATH)
|
549
611
|
DEFAULT_CFG_KEYS = DEFAULT_CFG_DICT.keys()
|
550
612
|
DEFAULT_CFG = IterableSimpleNamespace(**DEFAULT_CFG_DICT)
|
551
613
|
|
@@ -0,0 +1,175 @@
|
|
1
|
+
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
2
|
+
|
3
|
+
|
4
|
+
from ultralytics.utils import LOGGER
|
5
|
+
from ultralytics.utils.checks import check_requirements
|
6
|
+
|
7
|
+
|
8
|
+
class GPUInfo:
|
9
|
+
"""
|
10
|
+
Manages NVIDIA GPU information via pynvml with robust error handling.
|
11
|
+
|
12
|
+
Provides methods to query detailed GPU statistics (utilization, memory, temp, power) and select the most idle
|
13
|
+
GPUs based on configurable criteria. It safely handles the absence or initialization failure of the pynvml
|
14
|
+
library by logging warnings and disabling related features, preventing application crashes.
|
15
|
+
|
16
|
+
Includes fallback logic using `torch.cuda` for basic device counting if NVML is unavailable during GPU
|
17
|
+
selection. Manages NVML initialization and shutdown internally.
|
18
|
+
|
19
|
+
Attributes:
|
20
|
+
pynvml (module | None): The `pynvml` module if successfully imported and initialized, otherwise `None`.
|
21
|
+
nvml_available (bool): Indicates if `pynvml` is ready for use. True if import and `nvmlInit()` succeeded,
|
22
|
+
False otherwise.
|
23
|
+
gpu_stats (list[dict]): A list of dictionaries, each holding stats for one GPU. Populated on initialization
|
24
|
+
and by `refresh_stats()`. Keys include: 'index', 'name', 'utilization' (%), 'memory_used' (MiB),
|
25
|
+
'memory_total' (MiB), 'memory_free' (MiB), 'temperature' (C), 'power_draw' (W),
|
26
|
+
'power_limit' (W or 'N/A'). Empty if NVML is unavailable or queries fail.
|
27
|
+
"""
|
28
|
+
|
29
|
+
def __init__(self):
|
30
|
+
"""Initializes GPUInfo, attempting to import and initialize pynvml."""
|
31
|
+
self.pynvml = None
|
32
|
+
self.nvml_available = False
|
33
|
+
self.gpu_stats = []
|
34
|
+
|
35
|
+
try:
|
36
|
+
check_requirements("pynvml>=12.0.0")
|
37
|
+
self.pynvml = __import__("pynvml")
|
38
|
+
self.pynvml.nvmlInit()
|
39
|
+
self.nvml_available = True
|
40
|
+
self.refresh_stats()
|
41
|
+
except Exception as e:
|
42
|
+
LOGGER.warning(f"Failed to initialize pynvml, GPU stats disabled: {e}")
|
43
|
+
|
44
|
+
def __del__(self):
|
45
|
+
"""Ensures NVML is shut down when the object is garbage collected."""
|
46
|
+
self.shutdown()
|
47
|
+
|
48
|
+
def shutdown(self):
|
49
|
+
"""Shuts down NVML if it was initialized."""
|
50
|
+
if self.nvml_available and self.pynvml:
|
51
|
+
try:
|
52
|
+
self.pynvml.nvmlShutdown()
|
53
|
+
except Exception:
|
54
|
+
pass
|
55
|
+
self.nvml_available = False
|
56
|
+
|
57
|
+
def refresh_stats(self):
|
58
|
+
"""Refreshes the internal gpu_stats list by querying NVML."""
|
59
|
+
self.gpu_stats = []
|
60
|
+
if not self.nvml_available or not self.pynvml:
|
61
|
+
return
|
62
|
+
|
63
|
+
try:
|
64
|
+
device_count = self.pynvml.nvmlDeviceGetCount()
|
65
|
+
for i in range(device_count):
|
66
|
+
self.gpu_stats.append(self._get_device_stats(i))
|
67
|
+
except Exception as e:
|
68
|
+
LOGGER.warning(f"Error during device query: {e}")
|
69
|
+
self.gpu_stats = []
|
70
|
+
|
71
|
+
def _get_device_stats(self, index):
|
72
|
+
"""Gets stats for a single GPU device."""
|
73
|
+
handle = self.pynvml.nvmlDeviceGetHandleByIndex(index)
|
74
|
+
memory = self.pynvml.nvmlDeviceGetMemoryInfo(handle)
|
75
|
+
util = self.pynvml.nvmlDeviceGetUtilizationRates(handle)
|
76
|
+
|
77
|
+
def safe_get(func, *args, default=-1, divisor=1):
|
78
|
+
try:
|
79
|
+
val = func(*args)
|
80
|
+
return val // divisor if divisor != 1 and isinstance(val, (int, float)) else val
|
81
|
+
except Exception:
|
82
|
+
return default
|
83
|
+
|
84
|
+
temp_type = getattr(self.pynvml, "NVML_TEMPERATURE_GPU", -1)
|
85
|
+
|
86
|
+
return {
|
87
|
+
"index": index,
|
88
|
+
"name": self.pynvml.nvmlDeviceGetName(handle),
|
89
|
+
"utilization": util.gpu if util else -1,
|
90
|
+
"memory_used": memory.used >> 20 if memory else -1,
|
91
|
+
"memory_total": memory.total >> 20 if memory else -1,
|
92
|
+
"memory_free": memory.free >> 20 if memory else -1,
|
93
|
+
"temperature": safe_get(self.pynvml.nvmlDeviceGetTemperature, handle, temp_type),
|
94
|
+
"power_draw": safe_get(self.pynvml.nvmlDeviceGetPowerUsage, handle, divisor=1000),
|
95
|
+
"power_limit": safe_get(self.pynvml.nvmlDeviceGetEnforcedPowerLimit, handle, divisor=1000),
|
96
|
+
}
|
97
|
+
|
98
|
+
def print_status(self):
|
99
|
+
"""Prints GPU status in a compact table format using current stats."""
|
100
|
+
self.refresh_stats()
|
101
|
+
if not self.gpu_stats:
|
102
|
+
LOGGER.warning("No GPU stats available.")
|
103
|
+
return
|
104
|
+
|
105
|
+
stats = self.gpu_stats
|
106
|
+
name_len = max(len(gpu.get("name", "N/A")) for gpu in stats)
|
107
|
+
hdr = f"{'Idx':<3} {'Name':<{name_len}} {'Util':>6} {'Mem (MiB)':>15} {'Temp':>5} {'Pwr (W)':>10}"
|
108
|
+
LOGGER.info(f"\n--- GPU Status ---\n{hdr}\n{'-' * len(hdr)}")
|
109
|
+
|
110
|
+
for gpu in stats:
|
111
|
+
u = f"{gpu['utilization']:>5}%" if gpu["utilization"] >= 0 else " N/A "
|
112
|
+
m = f"{gpu['memory_used']:>6}/{gpu['memory_total']:<6}" if gpu["memory_used"] >= 0 else " N/A / N/A "
|
113
|
+
t = f"{gpu['temperature']}C" if gpu["temperature"] >= 0 else " N/A "
|
114
|
+
p = f"{gpu['power_draw']:>3}/{gpu['power_limit']:<3}" if gpu["power_draw"] >= 0 else " N/A "
|
115
|
+
|
116
|
+
LOGGER.info(f"{gpu.get('index'):<3d} {gpu.get('name', 'N/A'):<{name_len}} {u:>6} {m:>15} {t:>5} {p:>10}")
|
117
|
+
|
118
|
+
LOGGER.info(f"{'-' * len(hdr)}\n")
|
119
|
+
|
120
|
+
def select_idle_gpu(self, count=1, min_memory_mb=0):
|
121
|
+
"""
|
122
|
+
Selects the 'count' most idle GPUs based on utilization and free memory.
|
123
|
+
|
124
|
+
Args:
|
125
|
+
count (int): The number of idle GPUs to select. Defaults to 1.
|
126
|
+
min_memory_mb (int): Minimum free memory required (MiB). Defaults to 0.
|
127
|
+
|
128
|
+
Returns:
|
129
|
+
(list[int]): Indices of the selected GPUs, sorted by idleness.
|
130
|
+
|
131
|
+
Notes:
|
132
|
+
Returns fewer than 'count' if not enough qualify or exist.
|
133
|
+
Returns basic CUDA indices if NVML fails. Empty list if no GPUs found.
|
134
|
+
"""
|
135
|
+
LOGGER.info(f"Searching for {count} idle GPUs with >= {min_memory_mb} MiB free memory...")
|
136
|
+
|
137
|
+
if count <= 0:
|
138
|
+
return []
|
139
|
+
|
140
|
+
self.refresh_stats()
|
141
|
+
if not self.gpu_stats:
|
142
|
+
LOGGER.warning("NVML stats unavailable.")
|
143
|
+
return []
|
144
|
+
|
145
|
+
# Filter and sort eligible GPUs
|
146
|
+
eligible_gpus = [
|
147
|
+
gpu
|
148
|
+
for gpu in self.gpu_stats
|
149
|
+
if gpu.get("memory_free", -1) >= min_memory_mb and gpu.get("utilization", -1) != -1
|
150
|
+
]
|
151
|
+
eligible_gpus.sort(key=lambda x: (x.get("utilization", 101), -x.get("memory_free", 0)))
|
152
|
+
|
153
|
+
# Select top 'count' indices
|
154
|
+
selected = [gpu["index"] for gpu in eligible_gpus[:count]]
|
155
|
+
|
156
|
+
if selected:
|
157
|
+
LOGGER.info(f"Selected idle CUDA devices {selected}")
|
158
|
+
else:
|
159
|
+
LOGGER.warning(f"No GPUs met criteria (Util != -1, Free Mem >= {min_memory_mb} MiB).")
|
160
|
+
|
161
|
+
return selected
|
162
|
+
|
163
|
+
|
164
|
+
if __name__ == "__main__":
|
165
|
+
required_free_mem = 2048 # Require 2GB free VRAM
|
166
|
+
num_gpus_to_select = 1
|
167
|
+
|
168
|
+
gpu_info = GPUInfo()
|
169
|
+
gpu_info.print_status()
|
170
|
+
|
171
|
+
selected = gpu_info.select_idle_gpu(count=num_gpus_to_select, min_memory_mb=required_free_mem)
|
172
|
+
if selected:
|
173
|
+
print(f"\n==> Using selected GPU indices: {selected}")
|
174
|
+
devices = [f"cuda:{idx}" for idx in selected]
|
175
|
+
print(f" Target devices: {devices}")
|
ultralytics/utils/benchmarks.py
CHANGED
@@ -36,12 +36,11 @@ from pathlib import Path
|
|
36
36
|
|
37
37
|
import numpy as np
|
38
38
|
import torch.cuda
|
39
|
-
import yaml
|
40
39
|
|
41
40
|
from ultralytics import YOLO, YOLOWorld
|
42
41
|
from ultralytics.cfg import TASK2DATA, TASK2METRIC
|
43
42
|
from ultralytics.engine.exporter import export_formats
|
44
|
-
from ultralytics.utils import ARM64, ASSETS, LINUX, LOGGER, MACOS, TQDM, WEIGHTS_DIR
|
43
|
+
from ultralytics.utils import ARM64, ASSETS, LINUX, LOGGER, MACOS, TQDM, WEIGHTS_DIR, YAML
|
45
44
|
from ultralytics.utils.checks import IS_PYTHON_3_13, check_imgsz, check_requirements, check_yolo, is_rockchip
|
46
45
|
from ultralytics.utils.downloads import safe_download
|
47
46
|
from ultralytics.utils.files import file_size
|
@@ -283,12 +282,10 @@ class RF100Benchmark:
|
|
283
282
|
@staticmethod
|
284
283
|
def fix_yaml(path):
|
285
284
|
"""Fix the train and validation paths in a given YAML file."""
|
286
|
-
|
287
|
-
yaml_data = yaml.safe_load(file)
|
285
|
+
yaml_data = YAML.load(path)
|
288
286
|
yaml_data["train"] = "train/images"
|
289
287
|
yaml_data["val"] = "valid/images"
|
290
|
-
|
291
|
-
yaml.safe_dump(yaml_data, file)
|
288
|
+
YAML.dump(yaml_data, path)
|
292
289
|
|
293
290
|
def evaluate(self, yaml_path, val_log_file, eval_log_file, list_ind):
|
294
291
|
"""
|
@@ -309,8 +306,7 @@ class RF100Benchmark:
|
|
309
306
|
>>> benchmark.evaluate("path/to/data.yaml", "path/to/val_log.txt", "path/to/eval_log.txt", 0)
|
310
307
|
"""
|
311
308
|
skip_symbols = ["🚀", "⚠️", "💡", "❌"]
|
312
|
-
|
313
|
-
class_names = yaml.safe_load(stream)["names"]
|
309
|
+
class_names = YAML.load(yaml_path)["names"]
|
314
310
|
with open(val_log_file, encoding="utf-8") as f:
|
315
311
|
lines = f.readlines()
|
316
312
|
eval_lines = []
|
@@ -403,7 +399,7 @@ class ProfileModels:
|
|
403
399
|
imgsz (int): Size of the image used during profiling.
|
404
400
|
half (bool): Flag to indicate whether to use FP16 half-precision for TensorRT profiling.
|
405
401
|
trt (bool): Flag to indicate whether to profile using TensorRT.
|
406
|
-
device (torch.device | None): Device used for profiling. If None, it is determined automatically.
|
402
|
+
device (torch.device | str | None): Device used for profiling. If None, it is determined automatically.
|
407
403
|
|
408
404
|
Notes:
|
409
405
|
FP16 'half' argument option removed for ONNX as slower on CPU than FP32.
|
@@ -421,7 +417,7 @@ class ProfileModels:
|
|
421
417
|
self.imgsz = imgsz
|
422
418
|
self.half = half
|
423
419
|
self.trt = trt # run TensorRT profiling
|
424
|
-
self.device = device
|
420
|
+
self.device = device if isinstance(device, torch.device) else select_device(device)
|
425
421
|
|
426
422
|
def run(self):
|
427
423
|
"""
|
ultralytics/utils/checks.py
CHANGED
@@ -305,7 +305,7 @@ def check_font(font="Arial.ttf"):
|
|
305
305
|
Returns:
|
306
306
|
(Path): Resolved font file path.
|
307
307
|
"""
|
308
|
-
from matplotlib import font_manager
|
308
|
+
from matplotlib import font_manager # scope for faster 'import ultralytics'
|
309
309
|
|
310
310
|
# Check USER_CONFIG_DIR
|
311
311
|
name = Path(font).name
|
@@ -454,7 +454,7 @@ def check_suffix(file="yolo11n.pt", suffix=".pt", msg=""):
|
|
454
454
|
"""
|
455
455
|
if file and suffix:
|
456
456
|
if isinstance(suffix, str):
|
457
|
-
suffix =
|
457
|
+
suffix = {suffix}
|
458
458
|
for f in file if isinstance(file, (list, tuple)) else [file]:
|
459
459
|
s = Path(f).suffix.lower().strip() # file suffix
|
460
460
|
if len(s):
|
@@ -608,7 +608,7 @@ def check_yolo(verbose=True, device=""):
|
|
608
608
|
|
609
609
|
Args:
|
610
610
|
verbose (bool): Whether to print verbose information.
|
611
|
-
device (str): Device to use for YOLO.
|
611
|
+
device (str | torch.device): Device to use for YOLO.
|
612
612
|
"""
|
613
613
|
import psutil
|
614
614
|
|
@@ -810,7 +810,7 @@ def print_args(args: Optional[dict] = None, show_file=True, show_func=False):
|
|
810
810
|
except ValueError:
|
811
811
|
file = Path(file).stem
|
812
812
|
s = (f"{file}: " if show_file else "") + (f"{func}: " if show_func else "")
|
813
|
-
LOGGER.info(colorstr(s) + ", ".join(f"{k}={strip_auth(v)}" for k, v in args.items()))
|
813
|
+
LOGGER.info(colorstr(s) + ", ".join(f"{k}={strip_auth(v)}" for k, v in sorted(args.items())))
|
814
814
|
|
815
815
|
|
816
816
|
def cuda_device_count() -> int:
|
ultralytics/utils/metrics.py
CHANGED
@@ -5,7 +5,6 @@ import math
|
|
5
5
|
import warnings
|
6
6
|
from pathlib import Path
|
7
7
|
|
8
|
-
import matplotlib.pyplot as plt
|
9
8
|
import numpy as np
|
10
9
|
import torch
|
11
10
|
|
@@ -418,7 +417,8 @@ class ConfusionMatrix:
|
|
418
417
|
names (tuple): Names of classes, used as labels on the plot.
|
419
418
|
on_plot (func): An optional callback to pass plots path and data when they are rendered.
|
420
419
|
"""
|
421
|
-
import
|
420
|
+
import matplotlib.pyplot as plt # scope for faster 'import ultralytics'
|
421
|
+
import seaborn
|
422
422
|
|
423
423
|
array = self.matrix / ((self.matrix.sum(0).reshape(1, -1) + 1e-9) if normalize else 1) # normalize columns
|
424
424
|
array[array < 0.005] = np.nan # don't annotate (would appear as 0.00)
|
@@ -479,6 +479,8 @@ def plot_pr_curve(px, py, ap, save_dir=Path("pr_curve.png"), names={}, on_plot=N
|
|
479
479
|
names (dict, optional): Dictionary mapping class indices to class names.
|
480
480
|
on_plot (callable, optional): Function to call after plot is saved.
|
481
481
|
"""
|
482
|
+
import matplotlib.pyplot as plt # scope for faster 'import ultralytics'
|
483
|
+
|
482
484
|
fig, ax = plt.subplots(1, 1, figsize=(9, 6), tight_layout=True)
|
483
485
|
py = np.stack(py, axis=1)
|
484
486
|
|
@@ -515,6 +517,8 @@ def plot_mc_curve(px, py, save_dir=Path("mc_curve.png"), names={}, xlabel="Confi
|
|
515
517
|
ylabel (str, optional): Y-axis label.
|
516
518
|
on_plot (callable, optional): Function to call after plot is saved.
|
517
519
|
"""
|
520
|
+
import matplotlib.pyplot as plt # scope for faster 'import ultralytics'
|
521
|
+
|
518
522
|
fig, ax = plt.subplots(1, 1, figsize=(9, 6), tight_layout=True)
|
519
523
|
|
520
524
|
if 0 < len(names) < 21: # display per-class legend if < 21 classes
|
ultralytics/utils/plotting.py
CHANGED
@@ -6,7 +6,6 @@ from pathlib import Path
|
|
6
6
|
from typing import Callable, Dict, List, Optional, Union
|
7
7
|
|
8
8
|
import cv2
|
9
|
-
import matplotlib.pyplot as plt
|
10
9
|
import numpy as np
|
11
10
|
import torch
|
12
11
|
from PIL import Image, ImageDraw, ImageFont
|
@@ -534,8 +533,9 @@ def plot_labels(boxes, cls, names=(), save_dir=Path(""), on_plot=None):
|
|
534
533
|
save_dir (Path, optional): Directory to save the plot.
|
535
534
|
on_plot (Callable, optional): Function to call after plot is saved.
|
536
535
|
"""
|
537
|
-
import
|
538
|
-
import
|
536
|
+
import matplotlib.pyplot as plt # scope for faster 'import ultralytics'
|
537
|
+
import pandas
|
538
|
+
import seaborn
|
539
539
|
|
540
540
|
# Filter matplotlib>=3.7.2 warning and Seaborn use_inf and is_categorical FutureWarnings
|
541
541
|
warnings.filterwarnings("ignore", category=UserWarning, message="The figure layout has changed to tight")
|
@@ -819,7 +819,8 @@ def plot_results(file="path/to/results.csv", dir="", segment=False, pose=False,
|
|
819
819
|
>>> from ultralytics.utils.plotting import plot_results
|
820
820
|
>>> plot_results("path/to/results.csv", segment=True)
|
821
821
|
"""
|
822
|
-
import
|
822
|
+
import matplotlib.pyplot as plt # scope for faster 'import ultralytics'
|
823
|
+
import pandas as pd
|
823
824
|
from scipy.ndimage import gaussian_filter1d
|
824
825
|
|
825
826
|
save_dir = Path(file).parent if file else Path(dir)
|
@@ -878,6 +879,8 @@ def plt_color_scatter(v, f, bins=20, cmap="viridis", alpha=0.8, edgecolors="none
|
|
878
879
|
>>> f = np.random.rand(100)
|
879
880
|
>>> plt_color_scatter(v, f)
|
880
881
|
"""
|
882
|
+
import matplotlib.pyplot as plt # scope for faster 'import ultralytics'
|
883
|
+
|
881
884
|
# Calculate 2D histogram and corresponding colors
|
882
885
|
hist, xedges, yedges = np.histogram2d(v, f, bins=bins)
|
883
886
|
colors = [
|
@@ -903,7 +906,8 @@ def plot_tune_results(csv_file="tune_results.csv"):
|
|
903
906
|
Examples:
|
904
907
|
>>> plot_tune_results("path/to/tune_results.csv")
|
905
908
|
"""
|
906
|
-
import
|
909
|
+
import matplotlib.pyplot as plt # scope for faster 'import ultralytics'
|
910
|
+
import pandas as pd
|
907
911
|
from scipy.ndimage import gaussian_filter1d
|
908
912
|
|
909
913
|
def _save_one_file(file):
|
@@ -980,6 +984,8 @@ def feature_visualization(x, module_type, stage, n=32, save_dir=Path("runs/detec
|
|
980
984
|
n (int, optional): Maximum number of feature maps to plot.
|
981
985
|
save_dir (Path, optional): Directory to save results.
|
982
986
|
"""
|
987
|
+
import matplotlib.pyplot as plt # scope for faster 'import ultralytics'
|
988
|
+
|
983
989
|
for m in {"Detect", "Segment", "Pose", "Classify", "OBB", "RTDETRDecoder"}: # all model heads
|
984
990
|
if m in module_type:
|
985
991
|
return
|