feat(data): support cache ram of COCO dataset (#1562)
Browse files- requirements.txt +1 -0
- setup.cfg +1 -1
- tools/train.py +7 -4
- yolox/core/trainer.py +5 -2
- yolox/data/datasets/coco.py +84 -58
- yolox/exp/yolox_base.py +43 -14
- yolox/utils/metric.py +11 -0
requirements.txt
CHANGED
@@ -8,6 +8,7 @@ torchvision
|
|
8 |
thop
|
9 |
ninja
|
10 |
tabulate
|
|
|
11 |
|
12 |
# verified versions
|
13 |
# pycocotools corresponds to https://github.com/ppwwyyxx/cocoapi
|
|
|
8 |
thop
|
9 |
ninja
|
10 |
tabulate
|
11 |
+
psutil
|
12 |
|
13 |
# verified versions
|
14 |
# pycocotools corresponds to https://github.com/ppwwyyxx/cocoapi
|
setup.cfg
CHANGED
@@ -3,7 +3,7 @@ line_length = 100
|
|
3 |
multi_line_output = 3
|
4 |
balanced_wrapping = True
|
5 |
known_standard_library = setuptools
|
6 |
-
known_third_party = tqdm,loguru,tabulate
|
7 |
known_data_processing = cv2,numpy,scipy,PIL,matplotlib
|
8 |
known_datasets = pycocotools
|
9 |
known_deeplearning = torch,torchvision,caffe2,onnx,apex,timm,thop,torch2trt,tensorrt,openvino,onnxruntime
|
|
|
3 |
multi_line_output = 3
|
4 |
balanced_wrapping = True
|
5 |
known_standard_library = setuptools
|
6 |
+
known_third_party = tqdm,loguru,tabulate,psutil
|
7 |
known_data_processing = cv2,numpy,scipy,PIL,matplotlib
|
8 |
known_datasets = pycocotools
|
9 |
known_deeplearning = torch,torchvision,caffe2,onnx,apex,timm,thop,torch2trt,tensorrt,openvino,onnxruntime
|
tools/train.py
CHANGED
@@ -67,10 +67,10 @@ def make_parser():
|
|
67 |
)
|
68 |
parser.add_argument(
|
69 |
"--cache",
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
help="Caching imgs to
|
74 |
)
|
75 |
parser.add_argument(
|
76 |
"-o",
|
@@ -130,6 +130,9 @@ if __name__ == "__main__":
|
|
130 |
num_gpu = get_num_devices() if args.devices is None else args.devices
|
131 |
assert num_gpu <= get_num_devices()
|
132 |
|
|
|
|
|
|
|
133 |
dist_url = "auto" if args.dist_url is None else args.dist_url
|
134 |
launch(
|
135 |
main,
|
|
|
67 |
)
|
68 |
parser.add_argument(
|
69 |
"--cache",
|
70 |
+
type=str,
|
71 |
+
nargs="?",
|
72 |
+
const="ram",
|
73 |
+
help="Caching imgs to ram/disk for fast training.",
|
74 |
)
|
75 |
parser.add_argument(
|
76 |
"-o",
|
|
|
130 |
num_gpu = get_num_devices() if args.devices is None else args.devices
|
131 |
assert num_gpu <= get_num_devices()
|
132 |
|
133 |
+
if args.cache is not None:
|
134 |
+
exp.create_cache_dataset(args.cache)
|
135 |
+
|
136 |
dist_url = "auto" if args.dist_url is None else args.dist_url
|
137 |
launch(
|
138 |
main,
|
yolox/core/trainer.py
CHANGED
@@ -26,6 +26,7 @@ from yolox.utils import (
|
|
26 |
gpu_mem_usage,
|
27 |
is_parallel,
|
28 |
load_ckpt,
|
|
|
29 |
occupy_mem,
|
30 |
save_checkpoint,
|
31 |
setup_logger,
|
@@ -250,10 +251,12 @@ class Trainer:
|
|
250 |
["{}: {:.3f}s".format(k, v.avg) for k, v in time_meter.items()]
|
251 |
)
|
252 |
|
|
|
|
|
253 |
logger.info(
|
254 |
-
"{},
|
255 |
progress_str,
|
256 |
-
|
257 |
time_str,
|
258 |
loss_str,
|
259 |
self.meter["lr"].latest,
|
|
|
26 |
gpu_mem_usage,
|
27 |
is_parallel,
|
28 |
load_ckpt,
|
29 |
+
mem_usage,
|
30 |
occupy_mem,
|
31 |
save_checkpoint,
|
32 |
setup_logger,
|
|
|
251 |
["{}: {:.3f}s".format(k, v.avg) for k, v in time_meter.items()]
|
252 |
)
|
253 |
|
254 |
+
mem_str = "gpu mem: {:.0f}Mb, mem: {:.1f}Gb".format(gpu_mem_usage(), mem_usage())
|
255 |
+
|
256 |
logger.info(
|
257 |
+
"{}, {}, {}, {}, lr: {:.3e}".format(
|
258 |
progress_str,
|
259 |
+
mem_str,
|
260 |
time_str,
|
261 |
loss_str,
|
262 |
self.meter["lr"].latest,
|
yolox/data/datasets/coco.py
CHANGED
@@ -1,9 +1,13 @@
|
|
1 |
#!/usr/bin/env python3
|
2 |
# -*- coding:utf-8 -*-
|
3 |
# Copyright (c) Megvii, Inc. and its affiliates.
|
4 |
-
|
5 |
import os
|
|
|
|
|
|
|
6 |
from loguru import logger
|
|
|
7 |
|
8 |
import cv2
|
9 |
import numpy as np
|
@@ -45,6 +49,7 @@ class COCODataset(Dataset):
|
|
45 |
img_size=(416, 416),
|
46 |
preproc=None,
|
47 |
cache=False,
|
|
|
48 |
):
|
49 |
"""
|
50 |
COCO dataset initialization. Annotation data are read into memory by COCO API.
|
@@ -64,74 +69,95 @@ class COCODataset(Dataset):
|
|
64 |
self.coco = COCO(os.path.join(self.data_dir, "annotations", self.json_file))
|
65 |
remove_useless_info(self.coco)
|
66 |
self.ids = self.coco.getImgIds()
|
|
|
67 |
self.class_ids = sorted(self.coco.getCatIds())
|
68 |
self.cats = self.coco.loadCats(self.coco.getCatIds())
|
69 |
self._classes = tuple([c["name"] for c in self.cats])
|
70 |
-
self.imgs = None
|
71 |
self.name = name
|
72 |
self.img_size = img_size
|
73 |
self.preproc = preproc
|
74 |
self.annotations = self._load_coco_annotations()
|
75 |
-
|
|
|
|
|
|
|
|
|
76 |
self._cache_images()
|
77 |
|
78 |
-
def
|
79 |
-
|
|
|
|
|
80 |
|
81 |
-
|
82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
83 |
|
84 |
-
|
85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
86 |
|
87 |
-
def _cache_images(self):
|
88 |
-
logger.warning(
|
89 |
-
"\n********************************************************************************\n"
|
90 |
-
"You are using cached images in RAM to accelerate training.\n"
|
91 |
-
"This requires large system RAM.\n"
|
92 |
-
"Make sure you have 200G+ RAM and 136G available disk space for training COCO.\n"
|
93 |
-
"********************************************************************************\n"
|
94 |
-
)
|
95 |
-
max_h = self.img_size[0]
|
96 |
-
max_w = self.img_size[1]
|
97 |
-
cache_file = os.path.join(self.data_dir, f"img_resized_cache_{self.name}.array")
|
98 |
-
if not os.path.exists(cache_file):
|
99 |
logger.info(
|
100 |
-
"Caching images for the first time.
|
|
|
101 |
)
|
102 |
-
self.imgs = np.memmap(
|
103 |
-
cache_file,
|
104 |
-
shape=(len(self.ids), max_h, max_w, 3),
|
105 |
-
dtype=np.uint8,
|
106 |
-
mode="w+",
|
107 |
-
)
|
108 |
-
from tqdm import tqdm
|
109 |
-
from multiprocessing.pool import ThreadPool
|
110 |
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
)
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
|
|
|
|
|
|
120 |
pbar.close()
|
121 |
-
else:
|
122 |
-
logger.warning(
|
123 |
-
"You are using cached imgs! Make sure your dataset is not changed!!\n"
|
124 |
-
"Everytime the self.input_size is changed in your exp file, you need to delete\n"
|
125 |
-
"the cached data and re-generate them.\n"
|
126 |
-
)
|
127 |
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
135 |
|
136 |
def load_anno_from_ids(self, id_):
|
137 |
im_ann = self.coco.loadImgs(id_)[0]
|
@@ -152,7 +178,6 @@ class COCODataset(Dataset):
|
|
152 |
num_objs = len(objs)
|
153 |
|
154 |
res = np.zeros((num_objs, 5))
|
155 |
-
|
156 |
for ix, obj in enumerate(objs):
|
157 |
cls = self.class_ids.index(obj["category_id"])
|
158 |
res[ix, 0:4] = obj["clean_bbox"]
|
@@ -197,15 +222,16 @@ class COCODataset(Dataset):
|
|
197 |
|
198 |
def pull_item(self, index):
|
199 |
id_ = self.ids[index]
|
|
|
200 |
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
img =
|
205 |
else:
|
206 |
img = self.load_resized_img(index)
|
207 |
|
208 |
-
return img,
|
209 |
|
210 |
@Dataset.mosaic_getitem
|
211 |
def __getitem__(self, index):
|
|
|
1 |
#!/usr/bin/env python3
|
2 |
# -*- coding:utf-8 -*-
|
3 |
# Copyright (c) Megvii, Inc. and its affiliates.
|
4 |
+
import copy
|
5 |
import os
|
6 |
+
import random
|
7 |
+
from multiprocessing.pool import ThreadPool
|
8 |
+
import psutil
|
9 |
from loguru import logger
|
10 |
+
from tqdm import tqdm
|
11 |
|
12 |
import cv2
|
13 |
import numpy as np
|
|
|
49 |
img_size=(416, 416),
|
50 |
preproc=None,
|
51 |
cache=False,
|
52 |
+
cache_type="ram",
|
53 |
):
|
54 |
"""
|
55 |
COCO dataset initialization. Annotation data are read into memory by COCO API.
|
|
|
69 |
self.coco = COCO(os.path.join(self.data_dir, "annotations", self.json_file))
|
70 |
remove_useless_info(self.coco)
|
71 |
self.ids = self.coco.getImgIds()
|
72 |
+
self.num_imgs = len(self.ids)
|
73 |
self.class_ids = sorted(self.coco.getCatIds())
|
74 |
self.cats = self.coco.loadCats(self.coco.getCatIds())
|
75 |
self._classes = tuple([c["name"] for c in self.cats])
|
|
|
76 |
self.name = name
|
77 |
self.img_size = img_size
|
78 |
self.preproc = preproc
|
79 |
self.annotations = self._load_coco_annotations()
|
80 |
+
self.imgs = None
|
81 |
+
self.cache = cache
|
82 |
+
self.cache_type = cache_type
|
83 |
+
|
84 |
+
if self.cache:
|
85 |
self._cache_images()
|
86 |
|
87 |
+
def _cache_images(self):
|
88 |
+
mem = psutil.virtual_memory()
|
89 |
+
mem_required = self.cal_cache_ram()
|
90 |
+
gb = 1 << 30
|
91 |
|
92 |
+
if self.cache_type == "ram" and mem_required > mem.available:
|
93 |
+
self.cache = False
|
94 |
+
else:
|
95 |
+
logger.info(
|
96 |
+
f"{mem_required / gb:.1f}GB RAM required, "
|
97 |
+
f"{mem.available / gb:.1f}/{mem.total / gb:.1f}GB RAM available, "
|
98 |
+
f"Since the first thing we do is cache, "
|
99 |
+
f"there is no guarantee that the remaining memory space is sufficient"
|
100 |
+
)
|
101 |
|
102 |
+
if self.cache and self.imgs is None:
|
103 |
+
if self.cache_type == 'ram':
|
104 |
+
self.imgs = [None] * self.num_imgs
|
105 |
+
logger.info("You are using cached images in RAM to accelerate training!")
|
106 |
+
else: # 'disk'
|
107 |
+
self.cache_dir = os.path.join(
|
108 |
+
self.data_dir,
|
109 |
+
f"{self.name}_cache{self.img_size[0]}x{self.img_size[1]}"
|
110 |
+
)
|
111 |
+
if not os.path.exists(self.cache_dir):
|
112 |
+
os.mkdir(self.cache_dir)
|
113 |
+
logger.warning(
|
114 |
+
f"\n*******************************************************************\n"
|
115 |
+
f"You are using cached images in DISK to accelerate training.\n"
|
116 |
+
f"This requires large DISK space.\n"
|
117 |
+
f"Make sure you have {mem_required / gb:.1f} "
|
118 |
+
f"available DISK space for training COCO.\n"
|
119 |
+
f"*******************************************************************\\n"
|
120 |
+
)
|
121 |
+
else:
|
122 |
+
logger.info("Found disk cache!")
|
123 |
+
return
|
124 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
125 |
logger.info(
|
126 |
+
"Caching images for the first time. "
|
127 |
+
"This might take about 15 minutes for COCO"
|
128 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
129 |
|
130 |
+
num_threads = min(8, max(1, os.cpu_count() - 1))
|
131 |
+
b = 0
|
132 |
+
load_imgs = ThreadPool(num_threads).imap(self.load_resized_img, range(self.num_imgs))
|
133 |
+
pbar = tqdm(enumerate(load_imgs), total=self.num_imgs)
|
134 |
+
for i, x in pbar: # x = self.load_resized_img(self, i)
|
135 |
+
if self.cache_type == 'ram':
|
136 |
+
self.imgs[i] = x
|
137 |
+
else: # 'disk'
|
138 |
+
cache_filename = f'{self.annotations[i]["filename"].split(".")[0]}.npy'
|
139 |
+
np.save(os.path.join(self.cache_dir, cache_filename), x)
|
140 |
+
b += x.nbytes
|
141 |
+
pbar.desc = f'Caching images ({b / gb:.1f}/{mem_required / gb:.1f}GB {self.cache})'
|
142 |
pbar.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
143 |
|
144 |
+
def cal_cache_ram(self):
|
145 |
+
cache_bytes = 0
|
146 |
+
num_samples = min(self.num_imgs, 32)
|
147 |
+
for _ in range(num_samples):
|
148 |
+
img = self.load_resized_img(random.randint(0, self.num_imgs - 1))
|
149 |
+
cache_bytes += img.nbytes
|
150 |
+
mem_required = cache_bytes * self.num_imgs / num_samples
|
151 |
+
return mem_required
|
152 |
+
|
153 |
+
def __len__(self):
|
154 |
+
return self.num_imgs
|
155 |
+
|
156 |
+
def __del__(self):
|
157 |
+
del self.imgs
|
158 |
+
|
159 |
+
def _load_coco_annotations(self):
|
160 |
+
return [self.load_anno_from_ids(_ids) for _ids in self.ids]
|
161 |
|
162 |
def load_anno_from_ids(self, id_):
|
163 |
im_ann = self.coco.loadImgs(id_)[0]
|
|
|
178 |
num_objs = len(objs)
|
179 |
|
180 |
res = np.zeros((num_objs, 5))
|
|
|
181 |
for ix, obj in enumerate(objs):
|
182 |
cls = self.class_ids.index(obj["category_id"])
|
183 |
res[ix, 0:4] = obj["clean_bbox"]
|
|
|
222 |
|
223 |
def pull_item(self, index):
|
224 |
id_ = self.ids[index]
|
225 |
+
label, origin_image_size, _, filename = self.annotations[index]
|
226 |
|
227 |
+
if self.cache_type == 'ram':
|
228 |
+
img = self.imgs[index]
|
229 |
+
elif self.cache_type == 'disk':
|
230 |
+
img = np.load(os.path.join(self.cache_dir, f"{filename.split('.')[0]}.npy"))
|
231 |
else:
|
232 |
img = self.load_resized_img(index)
|
233 |
|
234 |
+
return copy.deepcopy(img), copy.deepcopy(label), origin_image_size, np.array([id_])
|
235 |
|
236 |
@Dataset.mosaic_getitem
|
237 |
def __getitem__(self, index):
|
yolox/exp/yolox_base.py
CHANGED
@@ -106,6 +106,23 @@ class Exp(BaseExp):
|
|
106 |
self.test_conf = 0.01
|
107 |
# nms threshold
|
108 |
self.nmsthre = 0.65
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
109 |
|
110 |
def get_model(self):
|
111 |
from yolox.models import YOLOX, YOLOPAFPN, YOLOXHead
|
@@ -127,7 +144,16 @@ class Exp(BaseExp):
|
|
127 |
self.model.train()
|
128 |
return self.model
|
129 |
|
130 |
-
def get_data_loader(self, batch_size, is_distributed, no_aug=False, cache_img=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
131 |
from yolox.data import (
|
132 |
COCODataset,
|
133 |
TrainTransform,
|
@@ -140,18 +166,23 @@ class Exp(BaseExp):
|
|
140 |
from yolox.utils import wait_for_the_master
|
141 |
|
142 |
with wait_for_the_master():
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
|
|
|
|
|
|
|
|
|
|
153 |
|
154 |
-
dataset = MosaicDetection(
|
155 |
dataset,
|
156 |
mosaic=not no_aug,
|
157 |
img_size=self.input_size,
|
@@ -169,8 +200,6 @@ class Exp(BaseExp):
|
|
169 |
mixup_prob=self.mixup_prob,
|
170 |
)
|
171 |
|
172 |
-
self.dataset = dataset
|
173 |
-
|
174 |
if is_distributed:
|
175 |
batch_size = batch_size // dist.get_world_size()
|
176 |
|
|
|
106 |
self.test_conf = 0.01
|
107 |
# nms threshold
|
108 |
self.nmsthre = 0.65
|
109 |
+
self.cache_dataset = None
|
110 |
+
self.dataset = None
|
111 |
+
|
112 |
+
def create_cache_dataset(self, cache_type: str = "ram"):
|
113 |
+
from yolox.data import COCODataset, TrainTransform
|
114 |
+
self.cache_dataset = COCODataset(
|
115 |
+
data_dir=self.data_dir,
|
116 |
+
json_file=self.train_ann,
|
117 |
+
img_size=self.input_size,
|
118 |
+
preproc=TrainTransform(
|
119 |
+
max_labels=50,
|
120 |
+
flip_prob=self.flip_prob,
|
121 |
+
hsv_prob=self.hsv_prob
|
122 |
+
),
|
123 |
+
cache=True,
|
124 |
+
cache_type=cache_type,
|
125 |
+
)
|
126 |
|
127 |
def get_model(self):
|
128 |
from yolox.models import YOLOX, YOLOPAFPN, YOLOXHead
|
|
|
144 |
self.model.train()
|
145 |
return self.model
|
146 |
|
147 |
+
def get_data_loader(self, batch_size, is_distributed, no_aug=False, cache_img: str = None):
|
148 |
+
"""
|
149 |
+
Get dataloader according to cache_img parameter.
|
150 |
+
Args:
|
151 |
+
no_aug (bool, optional): Whether to turn off mosaic data enhancement. Defaults to False.
|
152 |
+
cache_img (str, optional): cache_img is equivalent to cache_type. Defaults to None.
|
153 |
+
"ram" : Caching imgs to ram for fast training.
|
154 |
+
"disk": Caching imgs to disk for fast training.
|
155 |
+
None: Do not use cache, in this case cache_data is also None.
|
156 |
+
"""
|
157 |
from yolox.data import (
|
158 |
COCODataset,
|
159 |
TrainTransform,
|
|
|
166 |
from yolox.utils import wait_for_the_master
|
167 |
|
168 |
with wait_for_the_master():
|
169 |
+
if self.cache_dataset is None:
|
170 |
+
assert cache_img is None, "cache is True, but cache_dataset is None"
|
171 |
+
dataset = COCODataset(
|
172 |
+
data_dir=self.data_dir,
|
173 |
+
json_file=self.train_ann,
|
174 |
+
img_size=self.input_size,
|
175 |
+
preproc=TrainTransform(
|
176 |
+
max_labels=50,
|
177 |
+
flip_prob=self.flip_prob,
|
178 |
+
hsv_prob=self.hsv_prob),
|
179 |
+
cache=False,
|
180 |
+
cache_type=cache_img,
|
181 |
+
)
|
182 |
+
else:
|
183 |
+
dataset = self.cache_dataset
|
184 |
|
185 |
+
self.dataset = MosaicDetection(
|
186 |
dataset,
|
187 |
mosaic=not no_aug,
|
188 |
img_size=self.input_size,
|
|
|
200 |
mixup_prob=self.mixup_prob,
|
201 |
)
|
202 |
|
|
|
|
|
203 |
if is_distributed:
|
204 |
batch_size = batch_size // dist.get_world_size()
|
205 |
|
yolox/utils/metric.py
CHANGED
@@ -5,6 +5,7 @@ import functools
|
|
5 |
import os
|
6 |
import time
|
7 |
from collections import defaultdict, deque
|
|
|
8 |
|
9 |
import numpy as np
|
10 |
|
@@ -16,6 +17,7 @@ __all__ = [
|
|
16 |
"get_total_and_free_memory_in_Mb",
|
17 |
"occupy_mem",
|
18 |
"gpu_mem_usage",
|
|
|
19 |
]
|
20 |
|
21 |
|
@@ -51,6 +53,15 @@ def gpu_mem_usage():
|
|
51 |
return mem_usage_bytes / (1024 * 1024)
|
52 |
|
53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
54 |
class AverageMeter:
|
55 |
"""Track a series of values and provide access to smoothed values over a
|
56 |
window or the global series average.
|
|
|
5 |
import os
|
6 |
import time
|
7 |
from collections import defaultdict, deque
|
8 |
+
import psutil
|
9 |
|
10 |
import numpy as np
|
11 |
|
|
|
17 |
"get_total_and_free_memory_in_Mb",
|
18 |
"occupy_mem",
|
19 |
"gpu_mem_usage",
|
20 |
+
"mem_usage"
|
21 |
]
|
22 |
|
23 |
|
|
|
53 |
return mem_usage_bytes / (1024 * 1024)
|
54 |
|
55 |
|
56 |
+
def mem_usage():
|
57 |
+
"""
|
58 |
+
Compute the memory usage for the current machine (GB).
|
59 |
+
"""
|
60 |
+
gb = 1 << 30
|
61 |
+
mem = psutil.virtual_memory()
|
62 |
+
return mem.used / gb
|
63 |
+
|
64 |
+
|
65 |
class AverageMeter:
|
66 |
"""Track a series of values and provide access to smoothed values over a
|
67 |
window or the global series average.
|