From e094b6d5e8cd51c4e2d792f99a529c92424df535 Mon Sep 17 00:00:00 2001 From: "Wu, Jiantao (PG/R - Comp Sci & Elec Eng)" Date: Thu, 15 Feb 2024 11:29:42 +0000 Subject: [PATCH 01/31] Fix allocation query for decoding images --- ffcv/fields/rgb_image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffcv/fields/rgb_image.py b/ffcv/fields/rgb_image.py index b6420f11..dee275f7 100644 --- a/ffcv/fields/rgb_image.py +++ b/ffcv/fields/rgb_image.py @@ -161,7 +161,7 @@ def declare_state_and_memory(self, previous_state: State) -> Tuple[State, Alloca replace(previous_state, jit_mode=True, shape=output_shape, dtype=my_dtype), (AllocationQuery(output_shape, my_dtype), - AllocationQuery((self.max_height * self.max_width * np.uint64(3),), my_dtype), + AllocationQuery(((np.uint64(widths)*np.uint64(heights)*3).max(),), my_dtype), ) ) From b3e86c99dd66beb68eab9e16a46764ed979bc5c1 Mon Sep 17 00:00:00 2001 From: "Wu, Jiantao (PG/R - Comp Sci & Elec Eng)" Date: Thu, 15 Feb 2024 15:50:12 +0000 Subject: [PATCH 02/31] update useful tools --- examples/profiler.py | 134 +++++++++++++++++++++++++++++++++++++++++ examples/vis_loader.py | 42 +++++++++++++ 2 files changed, 176 insertions(+) create mode 100644 examples/profiler.py create mode 100644 examples/vis_loader.py diff --git a/examples/profiler.py b/examples/profiler.py new file mode 100644 index 00000000..daa180a9 --- /dev/null +++ b/examples/profiler.py @@ -0,0 +1,134 @@ +#%% +import time +from PIL import Image # a trick to solve loading lib problem +from ffcv import Loader +from ffcv.transforms import * +from ffcv.fields.decoders import RandomResizedCropRGBImageDecoder +import numpy as np +import ffcv +import argparse +from tqdm.auto import tqdm,trange +import torch.nn as nn + + +import json +from os import getpid +from psutil import Process, net_io_counters +import memory_profiler + + + +class ramqdm(tqdm): + """tqdm progress bar that reports RAM usage with each update""" + _empty_desc = "using ? GB RAM; ? CPU ? IO" + _desc = "{:.2f} GB RAM; {:.2f} % CPU {:.2f} MB IO" + _GB = 10**9 + """""" + def __init__(self, *args, **kwargs): + """Override desc and get reference to current process""" + if "desc" in kwargs: + # prepend desc to the reporter mask: + self._empty_desc = kwargs["desc"] + " " + self._empty_desc + self._desc = kwargs["desc"] + " " + self._desc + del kwargs["desc"] + else: + # nothing to prepend, reporter mask is at start of sentence: + self._empty_desc = self._empty_desc.capitalize() + self._desc = self._desc.capitalize() + super().__init__(*args, desc=self._empty_desc, **kwargs) + self._process = Process(getpid()) + self.metrics = [] + """""" + def update(self, n=1): + """Calculate RAM usage and update progress bar""" + rss = self._process.memory_info().rss + ps = self._process.cpu_percent() + io_counters = self._process.io_counters().read_bytes + # net_io = net_io_counters().bytes_recv + # io_counters += net_io + + current_desc = self._desc.format(rss/self._GB, ps, io_counters/1e6) + self.set_description(current_desc) + self.metrics.append({'mem':rss/self._GB, 'cpu':ps, 'io':io_counters/1e6}) + super().update(n) + + def summary(self): + res = {} + for key in self.metrics[0].keys(): + res[key] = np.mean([i[key] for i in self.metrics]) + return res + + +def load_one_epoch(args,loader): + start = time.time() + l=ramqdm(loader) + for batch in l: + pass + end = time.time() + res = l.summary() + throughput=args.repeat*loader.reader.num_samples/(end-start) + res['throughput'] = throughput + return res + + + +def main(args): + # pipe = ThreeAugmentPipeline() + from ffcv.pipeline import Pipeline, PipelineSpec, Compiler + pipe = { + "image":[ + RandomResizedCropRGBImageDecoder((args.img_size,args.img_size)), + RandomHorizontalFlip(), + # NormalizeImage([], np.float32), + ToTensor(), + ToTorchImage(), + ] + } + loader = Loader(args.data_path, batch_size=args.batch_size, num_workers=args.num_workers, os_cache=args.cache, pipelines=pipe,order=ffcv.loader.OrderOption.RANDOM, batches_ahead=0, distributed=False,seed=0,) + loader.pipeline_specs['image'] + # warmup + load_one_epoch(args,loader) + + for _ in range(args.repeat): + res = load_one_epoch(args,loader) + yield res + +#%% +if __name__ == '__main__': + parser = argparse.ArgumentParser(description="FFCV Profiler") + parser.add_argument("-r", "--repeat", type=int, default=5, help="number of samples to record one step for profile.") + parser.add_argument("-b", "--batch_size", type=int, default=128, help="batch size") + parser.add_argument("-p", "--data_path", type=str, help="data path", required=True) + parser.add_argument("--num_workers", type=int, default=10, help="number of workers") + parser.add_argument("--cache",default=False,action="store_true",help="cache data") + parser.add_argument("--exp", default=False, action="store_true", help="run experiments") + parser.add_argument("--img_size", type=int, default=224, help="image size") + parser.add_argument("--write_path", type=str, help='path to write result',default=None) + args = parser.parse_args() + if args.exp == False: + for res in main(args): + throughput = res['throughput'] + print(f"Throughput: {throughput:.2f} samples/s for {args.data_path}.") + res.update(args.__dict__) + if args.write_path: + with open(args.write_path,"a") as file: + file.write(json.dumps(res)+"\n") + else: + data = [] + with open(args.write_path,"a") as file: + for num_workers in [10,20,30,40,50,60]: + for cache in [True,False]: + for bs in [64,128,256]: + args.num_workers=num_workers + args.cache = cache + args.batch_size = bs + row = args.__dict__ + for res in main(args): + row.update(res) + file.write(json.dumps(row)+"\n") + print(row) + data.append(row) + import pandas as pd + df = pd.DataFrame(data) + print(df) + diff --git a/examples/vis_loader.py b/examples/vis_loader.py new file mode 100644 index 00000000..7ea1456c --- /dev/null +++ b/examples/vis_loader.py @@ -0,0 +1,42 @@ +import argparse +import time +from PIL import Image # a trick to solve loading lib problem +from ffcv import Loader +from ffcv.transforms import * +from ffcv.fields.decoders import CenterCropRGBImageDecoder + + +import numpy as np + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='FFCV Profiler') + parser.add_argument('data_path', type=str, default='data/imagenet', help='Path to the dataset') + parser.add_argument('--batch_size', type=int, default=16, help='Batch size') + parser.add_argument('--write_path', type=str, default='viz.png', help='Path to write result') + args = parser.parse_args() + + loader = Loader(args.data_path, batch_size=args.batch_size, num_workers=10, os_cache=True, pipelines={ + 'image':[CenterCropRGBImageDecoder((224, 224),3/4), ToTensor(), ToTorchImage()] + }, batches_ahead=0,) + + for x,_ in loader: + break + + print('Done') + num = int(np.sqrt(args.batch_size)) + import cv2 + + image = np.zeros((224*num, 224*num, 3), dtype=np.uint8) + for i in range(num): + for j in range(num): + if i*num+j >= args.batch_size: + break + img = x[i*num+j].numpy().transpose(1,2,0) + # img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) + image[i*224:(i+1)*224, j*224:(j+1)*224] = (img).astype(np.uint8) + + if args.write_path: + Image.fromarray(image).save(args.write_path) + + From 5c4f9cb8fdf57efd2261bfa89ac65a96907713df Mon Sep 17 00:00:00 2001 From: gent Date: Sat, 24 Feb 2024 19:48:01 +0000 Subject: [PATCH 03/31] add im crop_resize decode to avoid decode the whole images --- examples/docs_examples/linear_regression.py | 2 +- ffcv/fields/rgb_image.py | 59 +++--- ffcv/libffcv.py | 30 +++- ffcv/loader/loader.py | 14 +- libffcv/libffcv.cpp | 189 +++++++++++++++++++- setup.py | 4 +- tests/test_cuda_nonblocking.py | 2 +- 7 files changed, 263 insertions(+), 37 deletions(-) diff --git a/examples/docs_examples/linear_regression.py b/examples/docs_examples/linear_regression.py index f9a6e81c..5431448a 100644 --- a/examples/docs_examples/linear_regression.py +++ b/examples/docs_examples/linear_regression.py @@ -57,7 +57,7 @@ def __len__(self): train_loader = DataLoader(dataset, batch_size=2048, num_workers=8, shuffle=True) else: train_loader = Loader('/tmp/linreg_data.beton', batch_size=2048, - num_workers=8, order=OrderOption.QUASI_RANDOM, os_cache=False, + num_workers=8, order=OrderOption.QUASI_RANDOM, cache_type=1, pipelines={ 'covariate': [NDArrayDecoder(), ToTensor(), ToDevice(ch.device('cuda:0'))], 'label': [NDArrayDecoder(), ToTensor(), Squeeze(), ToDevice(ch.device('cuda:0'))] diff --git a/ffcv/fields/rgb_image.py b/ffcv/fields/rgb_image.py index dee275f7..ccdab23c 100644 --- a/ffcv/fields/rgb_image.py +++ b/ffcv/fields/rgb_image.py @@ -12,7 +12,7 @@ from ..pipeline.state import State from ..pipeline.compiler import Compiler from ..pipeline.allocation_query import AllocationQuery -from ..libffcv import imdecode, memcpy, resize_crop +from ..libffcv import * if TYPE_CHECKING: from ..memory_managers.base import MemoryManager @@ -101,14 +101,14 @@ def declare_state_and_memory(self, previous_state: State) -> Tuple[State, Alloca consider RandomResizedCropRGBImageDecoder or CenterCropRGBImageDecoder instead.""" raise TypeError(msg) - - biggest_shape = (max_height, max_width, 3) + + max_shape = ((np.uint64(widths)*np.uint64(heights)*3).max(),) my_dtype = np.dtype(' Callable: @@ -156,12 +156,12 @@ def declare_state_and_memory(self, previous_state: State) -> Tuple[State, Alloca self.max_height = np.uint64(heights.max()) output_shape = (self.output_size[0], self.output_size[1], 3) my_dtype = np.dtype(' Callable: my_range = Compiler.get_iterator() imdecode_c = Compiler.compile(imdecode) resize_crop_c = Compiler.compile(resize_crop) + imcropresizedecode_c = Compiler.compile(imcropresizedecode) get_crop_c = Compiler.compile(self.get_crop_generator) scale = self.scale ratio = self.ratio + tx,ty = self.output_size if isinstance(scale, tuple): scale = np.array(scale) if isinstance(ratio, tuple): @@ -191,22 +193,37 @@ def decode(batch_indices, my_storage, metadata, storage_state): height = np.uint32(field['height']) width = np.uint32(field['width']) + i, j, h, w = get_crop_c(height, width, scale, ratio) + # i, j = 10, 30 + # s = min(h, w) + # i, j = min(i,j), min(i,j) + # h, w = min(h,w), min(h,w) + # h, w = s - i, s - i if field['mode'] == jpg: temp_buffer = temp_storage[dst_ix] - imdecode_c(image_data, temp_buffer, - height, width, height, width, 0, 0, 1, 1, False, False) - selected_size = 3 * height * width - temp_buffer = temp_buffer.reshape(-1)[:selected_size] - temp_buffer = temp_buffer.reshape(height, width, 3) - + imcropresizedecode_c(image_data, temp_buffer, destination[dst_ix], + h,w, + i, j, ) + # imdecode_c(image_data, temp_buffer, + # height, width, height, width, 0, 0, 1, 1, False, False) + # selected_size = 3 * height * width + # temp_buffer = temp_buffer.reshape(-1)[:selected_size] + # temp_buffer = temp_buffer.reshape(height, width, 3) + + # resize_crop_c(temp_buffer, i, i + h, j, j + w, + # destination[dst_ix]) + + # selected_size = 3 * w * h + # temp_buffer = temp_buffer.reshape(-1)[:selected_size] + # temp_buffer = temp_buffer.reshape(h, w, 3) + # resize_crop_c(temp_buffer, 0, h, 0, w, + # destination[dst_ix],cv2.INTER_LINEAR) + else: temp_buffer = image_data.reshape(height, width, 3) - - i, j, h, w = get_crop_c(height, width, scale, ratio) - - resize_crop_c(temp_buffer, i, i + h, j, j + w, - destination[dst_ix]) - + resize_crop_c(temp_buffer, i, i + h, j, j + w, + destination[dst_ix]) + return destination[:len(batch_indices)] decode.is_parallel = True return decode @@ -311,10 +328,10 @@ def get_decoder_class(self) -> Type[Operation]: return SimpleRGBImageDecoder @staticmethod - def from_binary(binary: ARG_TYPE) -> Field: + def from_binary(binary: ARG_TYPE) -> Field: # type: ignore return RGBImageField() - def to_binary(self) -> ARG_TYPE: + def to_binary(self) -> ARG_TYPE: # type: ignore return np.zeros(1, dtype=ARG_TYPE)[0] def encode(self, destination, image, malloc): diff --git a/ffcv/libffcv.py b/ffcv/libffcv.py index 693269f6..7a47d4fe 100644 --- a/ffcv/libffcv.py +++ b/ffcv/libffcv.py @@ -20,15 +20,15 @@ def read(fileno:int, destination:np.ndarray, offset:int): ctypes_resize = lib.resize -ctypes_resize.argtypes = 11 * [c_int64] +ctypes_resize.argtypes = 12 * [c_int64] -def resize_crop(source, start_row, end_row, start_col, end_col, destination): +def resize_crop(source, start_row, end_row, start_col, end_col, destination,interpolation=3): ctypes_resize(0, source.ctypes.data, source.shape[0], source.shape[1], start_row, end_row, start_col, end_col, destination.ctypes.data, - destination.shape[0], destination.shape[1]) + destination.shape[0], destination.shape[1],interpolation) # Extract and define the interface of imdeocde ctypes_imdecode = lib.imdecode @@ -48,9 +48,31 @@ def imdecode(source: np.ndarray, dst: np.ndarray, enable_crop, do_flip) +# Extract and define the interface of imdeocde +ctypes_imcropresizedecode = lib.imcropresizedecode +ctypes_imcropresizedecode.argtypes = [ + c_void_p, c_uint64, + c_void_p, c_void_p, + c_uint32, c_uint32, + c_uint32, c_uint32, + c_uint32, c_uint32, + c_uint32, +] + +def imcropresizedecode(source: np.ndarray, tmp: np.ndarray, dst: np.ndarray, + crop_height: int, crop_width: int, + offset_y=0, offset_x=0, + interpolation=3): + return ctypes_imcropresizedecode( + source.ctypes.data, source.size, + tmp.ctypes.data, dst.ctypes.data, + dst.shape[0], dst.shape[1], + crop_height, crop_width, + offset_y, offset_x, + interpolation) + ctypes_memcopy = lib.my_memcpy ctypes_memcopy.argtypes = [c_void_p, c_void_p, c_uint64] def memcpy(source: np.ndarray, dest: np.ndarray): return ctypes_memcopy(source.ctypes.data, dest.ctypes.data, source.size*source.itemsize) - diff --git a/ffcv/loader/loader.py b/ffcv/loader/loader.py index aa8fd9dc..09d5cdbb 100644 --- a/ffcv/loader/loader.py +++ b/ffcv/loader/loader.py @@ -47,7 +47,7 @@ class OrderOption(Enum): } DEFAULT_PROCESS_CACHE = int(environ.get('FFCV_DEFAULT_CACHE_PROCESS', "0")) -DEFAULT_OS_CACHE = not DEFAULT_PROCESS_CACHE + class Loader: """FFCV loader class that can be used as a drop-in replacement @@ -90,7 +90,7 @@ def __init__(self, fname: str, batch_size: int, num_workers: int = -1, - os_cache: bool = DEFAULT_OS_CACHE, + cache_type: int = DEFAULT_PROCESS_CACHE, order: Union[ORDER_TYPE, TraversalOrder] = OrderOption.SEQUENTIAL, distributed: bool = False, seed: int = None, # For ordering of samples @@ -117,7 +117,7 @@ def __init__(self, 'fname': fname, 'batch_size': batch_size, 'num_workers': num_workers, - 'os_cache': os_cache, + 'os_cache': cache_type, 'order': order, 'distributed': distributed, 'seed': seed, @@ -148,11 +148,13 @@ def __init__(self, else: self.indices = np.array(indices) - if os_cache: - self.memory_manager: MemoryManager = OSCacheManager(self.reader) - else: + if cache_type == 0: self.memory_manager: MemoryManager = ProcessCacheManager( self.reader) + elif cache_type == 1: + self.memory_manager: MemoryManager = OSCacheManager(self.reader) + else: + raise ValueError("Unknown cache type. Use 0 for process cache, 1 for os cache, or 2 for no cache.") if order in ORDER_MAP: self.traversal_order: TraversalOrder = ORDER_MAP[order](self) diff --git a/libffcv/libffcv.cpp b/libffcv/libffcv.cpp index db4798d1..ab600151 100644 --- a/libffcv/libffcv.cpp +++ b/libffcv/libffcv.cpp @@ -16,6 +16,46 @@ #define EXPORT #endif +// #define _DEBUG +#ifdef _DEBUG +#define DBOUT std::cout // or any other ostream +#else +#define DBOUT 0 && std::cout +#endif + +#include // For std::pair + + +std::pair<__uint32_t, __uint32_t> axis_to_image_boundaries(int a, int b, int img_boundary, int mcuBlock) { + int img_b = img_boundary - (img_boundary % mcuBlock); + int delta_a = a % mcuBlock; + int rb = a + b; + // reduce the a to align the mcu block + if (a > img_b) { + a = img_b; + + } else { + a -= delta_a; + } + + // the b to align the mcu block + // b = rb + (mcuBlock - rb % mcuBlock) - a; + b += delta_a; + + if ((a + b) > img_b) { + b = img_b - a; + } + + return std::make_pair(a, b); +} + +struct Boundaries { + int x; + int y; + int h; + int w; +}; + extern "C" { // a key use to point to the tjtransform instance static pthread_key_t key_tj_transformer; @@ -32,13 +72,13 @@ extern "C" { EXPORT void resize(int64_t cresizer, int64_t source_p, int64_t sx, int64_t sy, int64_t start_row, int64_t end_row, int64_t start_col, int64_t end_col, - int64_t dest_p, int64_t tx, int64_t ty) { + int64_t dest_p, int64_t tx, int64_t ty, int64_t interpolation) { // TODO use proper arguments type cv::Mat source_matrix(sx, sy, CV_8UC3, (uint8_t*) source_p); cv::Mat dest_matrix(tx, ty, CV_8UC3, (uint8_t*) dest_p); cv::resize(source_matrix.colRange(start_col, end_col).rowRange(start_row, end_row), - dest_matrix, dest_matrix.size(), 0, 0, cv::INTER_AREA); + dest_matrix, dest_matrix.size(), 0, 0, interpolation); } EXPORT void my_memcpy(void *source, void* dst, uint64_t size) { @@ -127,4 +167,149 @@ extern "C" { PyMODINIT_FUNC PyInit__libffcv(void) { return PyModule_Create(&libffcvmodule); } + + + EXPORT int imcropresizedecode(unsigned char *input_buffer, __uint64_t input_size, + unsigned char *tmp_buffer, + unsigned char *output_buffer, + __uint32_t tar_height, __uint32_t tar_width, + __uint32_t crop_height, __uint32_t crop_width, + __uint32_t offset_y, __uint32_t offset_x, + __uint32_t interpolation + ) + { + pthread_once(&key_once, make_keys); + + tjhandle tj_transformer; + tjhandle tj_decompressor; + if ((tj_transformer = pthread_getspecific(key_tj_transformer)) == NULL) + { + tj_transformer = tjInitTransform(); + pthread_setspecific(key_tj_transformer, tj_transformer); + } + if ((tj_decompressor = pthread_getspecific(key_tj_decompressor)) == NULL) + { + tj_decompressor = tjInitDecompress(); + pthread_setspecific(key_tj_decompressor, tj_decompressor); + } + + + // get info about the cropped image + int width, height, subsamp, colorspace; + int result = tjDecompressHeader3(tj_decompressor, input_buffer, input_size, &width, &height, &subsamp, &colorspace); + if (result == -1) { + const char* error_message = tjGetErrorStr(); + // Handle the error + std::cerr << "Error in info: " << error_message << std::endl; + return -1; + } + else { + DBOUT << "width: " << width << " height: " << height << " Subsamp: " << subsamp << " Colorspace: " << colorspace << std::endl; + } + + // get the boundaries of the cropped image + std::pair x_boundaries = axis_to_image_boundaries(offset_x, crop_width, width, tjMCUWidth[subsamp]); + std::pair y_boundaries = axis_to_image_boundaries(offset_y, crop_height, height, tjMCUWidth[subsamp]); + + // reduce the crop size if it is out of the image boundaries + // int lbound = x_boundaries.first + x_boundaries.second; + // if(lbound "; + DBOUT << "x_boundaries: " << x_boundaries.first << ", " << x_boundaries.second << std::endl; + DBOUT << offset_x + crop_width << " <= " << x_boundaries.second+x_boundaries.first <<" <= " << width << std::endl; + + DBOUT << "offset_y: " << offset_y << ", " << crop_height << ", " < "; + DBOUT << "y_boundaries: " << y_boundaries.first << ", " << y_boundaries.second << std::endl; + DBOUT << offset_y + crop_height << " < " << y_boundaries.second+y_boundaries.first <<" <= " << height << std::endl; + + // if (x_boundaries.first>offset_x || x_boundaries.second+x_boundaries.firstoffset_y || y_boundaries.second+y_boundaries.first < crop_height+ offset_y){ + // std::cerr << "Invalid crop y offset" << std::endl; + // } + offset_x = x_boundaries.first; + offset_y = y_boundaries.first; + crop_width = x_boundaries.second; + crop_height = y_boundaries.second; + + // if it is not possible to crop the image, return the original image + if (crop_width<8){ + // std::cerr << "Invalid crop width " << crop_width < Date: Sat, 24 Feb 2024 20:05:28 +0000 Subject: [PATCH 04/31] remove crop for resize --- libffcv/libffcv.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libffcv/libffcv.cpp b/libffcv/libffcv.cpp index ab600151..9693149d 100644 --- a/libffcv/libffcv.cpp +++ b/libffcv/libffcv.cpp @@ -306,8 +306,8 @@ extern "C" { int dy = offset_y-y_boundaries.first; cv::resize((source_matrix - .colRange(dx,crop_width) - .rowRange(dy,crop_height) + // .colRange(dx,crop_width) + // .rowRange(dy,crop_height) ), dest_matrix, dest_matrix.size(), 0, 0, interpolation); return result; From 94f3a0bdcf8a092f243bc94454d6d6ba407617af Mon Sep 17 00:00:00 2001 From: gent Date: Sun, 25 Feb 2024 00:54:12 +0000 Subject: [PATCH 05/31] use "crop region" instead of transform in libffcv.py --- examples/profiler.py | 41 ++++++++++++++++---------- examples/vis_loader.py | 10 +++++-- ffcv/fields/rgb_image.py | 38 ++++++------------------ ffcv/libffcv.py | 3 +- ffcv/loader/loader.py | 4 +-- libffcv/libffcv.cpp | 62 +++++++++++++--------------------------- setup.py | 1 + 7 files changed, 67 insertions(+), 92 deletions(-) diff --git a/examples/profiler.py b/examples/profiler.py index daa180a9..f4e500da 100644 --- a/examples/profiler.py +++ b/examples/profiler.py @@ -1,22 +1,27 @@ #%% + import time -from PIL import Image # a trick to solve loading lib problem -from ffcv import Loader -from ffcv.transforms import * -from ffcv.fields.decoders import RandomResizedCropRGBImageDecoder +from PIL import Image# a trick to solve loading lib problem +from ffcv.fields.rgb_image import * +from ffcv.transforms import RandomHorizontalFlip, NormalizeImage, ToTensor, ToTorchImage, ToDevice import numpy as np + +from ffcv import Loader import ffcv import argparse from tqdm.auto import tqdm,trange import torch.nn as nn +import torch +from psutil import Process, net_io_counters - +# from torchvi import json from os import getpid -from psutil import Process, net_io_counters -import memory_profiler +from ffcv.transforms.ops import Convert +IMAGENET_MEAN = np.array([0.485, 0.456, 0.406]) * 255 +IMAGENET_STD = np.array([0.229, 0.224, 0.225]) * 255 class ramqdm(tqdm): """tqdm progress bar that reports RAM usage with each update""" @@ -66,26 +71,30 @@ def load_one_epoch(args,loader): pass end = time.time() res = l.summary() - throughput=args.repeat*loader.reader.num_samples/(end-start) + throughput=loader.reader.num_samples/(end-start) res['throughput'] = throughput + x1,y = batch + print("Mean: ", x1.mean().item(), "Std: ", x1.std().item()) return res def main(args): # pipe = ThreeAugmentPipeline() - from ffcv.pipeline import Pipeline, PipelineSpec, Compiler pipe = { - "image":[ - RandomResizedCropRGBImageDecoder((args.img_size,args.img_size)), + 'image': [CenterCropRGBImageDecoder((args.img_size,args.img_size), 0.875), RandomHorizontalFlip(), - # NormalizeImage([], np.float32), - ToTensor(), + ToTensor(), + ToDevice(torch.device('cuda')), ToTorchImage(), + Convert(torch.float16), + # NormalizeImage(IMAGENET_MEAN, IMAGENET_STD, np.float16), ] } - loader = Loader(args.data_path, batch_size=args.batch_size, num_workers=args.num_workers, os_cache=args.cache, pipelines=pipe,order=ffcv.loader.OrderOption.RANDOM, batches_ahead=0, distributed=False,seed=0,) - loader.pipeline_specs['image'] + loader = Loader(args.data_path, batch_size=args.batch_size, num_workers=args.num_workers, + pipelines=pipe,order=ffcv.loader.OrderOption.RANDOM, + batches_ahead=10, distributed=False,seed=0,) + # warmup load_one_epoch(args,loader) @@ -132,3 +141,5 @@ def main(args): df = pd.DataFrame(data) print(df) + + exit(0) \ No newline at end of file diff --git a/examples/vis_loader.py b/examples/vis_loader.py index 7ea1456c..69011e28 100644 --- a/examples/vis_loader.py +++ b/examples/vis_loader.py @@ -3,7 +3,7 @@ from PIL import Image # a trick to solve loading lib problem from ffcv import Loader from ffcv.transforms import * -from ffcv.fields.decoders import CenterCropRGBImageDecoder +from ffcv.fields.decoders import CenterCropRGBImageDecoder, RandomResizedCropRGBImageDecoder import numpy as np @@ -16,11 +16,15 @@ parser.add_argument('--write_path', type=str, default='viz.png', help='Path to write result') args = parser.parse_args() - loader = Loader(args.data_path, batch_size=args.batch_size, num_workers=10, os_cache=True, pipelines={ - 'image':[CenterCropRGBImageDecoder((224, 224),3/4), ToTensor(), ToTorchImage()] + loader = Loader(args.data_path, batch_size=args.batch_size, num_workers=10, cache_type=1, pipelines={ + 'image':[RandomResizedCropRGBImageDecoder((224, 224)), + ToTensor(), + ToTorchImage()] }, batches_ahead=0,) for x,_ in loader: + x1 = x.float() + print("Mean: ", x1.mean().item(), "Std: ", x1.std().item()) break print('Done') diff --git a/ffcv/fields/rgb_image.py b/ffcv/fields/rgb_image.py index ccdab23c..aa70a1cb 100644 --- a/ffcv/fields/rgb_image.py +++ b/ffcv/fields/rgb_image.py @@ -22,15 +22,14 @@ IMAGE_MODES['jpg'] = 0 IMAGE_MODES['raw'] = 1 - +from turbojpeg import TurboJPEG, TJCS_RGB +turbo_jpeg = TurboJPEG() def encode_jpeg(numpy_image, quality): - numpy_image = cv2.cvtColor(numpy_image, cv2.COLOR_RGB2BGR) - success, result = cv2.imencode('.jpg', numpy_image, - [int(cv2.IMWRITE_JPEG_QUALITY), quality]) - - if not success: - raise ValueError("Impossible to encode image in jpeg") - + # numpy_image = cv2.cvtColor(numpy_image, cv2.COLOR_RGB2BGR) + # success, result = cv2.imencode('.jpg', numpy_image, + # [int(cv2.IMWRITE_JPEG_QUALITY), quality]) + result = turbo_jpeg.encode(numpy_image, quality=quality, pixel_format=TJCS_RGB,) + result = np.frombuffer(result, np.uint8) return result.reshape(-1) @@ -194,31 +193,12 @@ def decode(batch_indices, my_storage, metadata, storage_state): width = np.uint32(field['width']) i, j, h, w = get_crop_c(height, width, scale, ratio) - # i, j = 10, 30 - # s = min(h, w) - # i, j = min(i,j), min(i,j) - # h, w = min(h,w), min(h,w) - # h, w = s - i, s - i + if field['mode'] == jpg: temp_buffer = temp_storage[dst_ix] imcropresizedecode_c(image_data, temp_buffer, destination[dst_ix], h,w, - i, j, ) - # imdecode_c(image_data, temp_buffer, - # height, width, height, width, 0, 0, 1, 1, False, False) - # selected_size = 3 * height * width - # temp_buffer = temp_buffer.reshape(-1)[:selected_size] - # temp_buffer = temp_buffer.reshape(height, width, 3) - - # resize_crop_c(temp_buffer, i, i + h, j, j + w, - # destination[dst_ix]) - - # selected_size = 3 * w * h - # temp_buffer = temp_buffer.reshape(-1)[:selected_size] - # temp_buffer = temp_buffer.reshape(h, w, 3) - # resize_crop_c(temp_buffer, 0, h, 0, w, - # destination[dst_ix],cv2.INTER_LINEAR) - + i, j, cv2.INTER_CUBIC) else: temp_buffer = image_data.reshape(height, width, 3) resize_crop_c(temp_buffer, i, i + h, j, j + w, diff --git a/ffcv/libffcv.py b/ffcv/libffcv.py index 7a47d4fe..79c72313 100644 --- a/ffcv/libffcv.py +++ b/ffcv/libffcv.py @@ -4,6 +4,7 @@ import platform from ctypes import CDLL, c_int64, c_uint8, c_uint64, POINTER, c_void_p, c_uint32, c_bool, cdll import ffcv._libffcv +import cv2 lib = CDLL(ffcv._libffcv.__file__) if platform.system() == "Windows": @@ -62,7 +63,7 @@ def imdecode(source: np.ndarray, dst: np.ndarray, def imcropresizedecode(source: np.ndarray, tmp: np.ndarray, dst: np.ndarray, crop_height: int, crop_width: int, offset_y=0, offset_x=0, - interpolation=3): + interpolation=cv2.INTER_CUBIC): return ctypes_imcropresizedecode( source.ctypes.data, source.size, tmp.ctypes.data, dst.ctypes.data, diff --git a/ffcv/loader/loader.py b/ffcv/loader/loader.py index 09d5cdbb..d0aa0c8a 100644 --- a/ffcv/loader/loader.py +++ b/ffcv/loader/loader.py @@ -149,10 +149,10 @@ def __init__(self, self.indices = np.array(indices) if cache_type == 0: + self.memory_manager: MemoryManager = OSCacheManager(self.reader) + elif cache_type == 1: self.memory_manager: MemoryManager = ProcessCacheManager( self.reader) - elif cache_type == 1: - self.memory_manager: MemoryManager = OSCacheManager(self.reader) else: raise ValueError("Unknown cache type. Use 0 for process cache, 1 for os cache, or 2 for no cache.") diff --git a/libffcv/libffcv.cpp b/libffcv/libffcv.cpp index 9693149d..40f2493f 100644 --- a/libffcv/libffcv.cpp +++ b/libffcv/libffcv.cpp @@ -184,19 +184,19 @@ extern "C" { tjhandle tj_decompressor; if ((tj_transformer = pthread_getspecific(key_tj_transformer)) == NULL) { - tj_transformer = tjInitTransform(); + tj_transformer = tj3Init(TJINIT_TRANSFORM); pthread_setspecific(key_tj_transformer, tj_transformer); } if ((tj_decompressor = pthread_getspecific(key_tj_decompressor)) == NULL) { - tj_decompressor = tjInitDecompress(); + tj_decompressor = tj3Init(TJINIT_DECOMPRESS); pthread_setspecific(key_tj_decompressor, tj_decompressor); } - + int result ; // get info about the cropped image int width, height, subsamp, colorspace; - int result = tjDecompressHeader3(tj_decompressor, input_buffer, input_size, &width, &height, &subsamp, &colorspace); + result = tjDecompressHeader3(tj_decompressor, input_buffer, input_size, &width, &height, &subsamp, &colorspace); if (result == -1) { const char* error_message = tjGetErrorStr(); // Handle the error @@ -212,14 +212,14 @@ extern "C" { std::pair y_boundaries = axis_to_image_boundaries(offset_y, crop_height, height, tjMCUWidth[subsamp]); // reduce the crop size if it is out of the image boundaries - // int lbound = x_boundaries.first + x_boundaries.second; - // if(lbound "; DBOUT << "x_boundaries: " << x_boundaries.first << ", " << x_boundaries.second << std::endl; @@ -229,13 +229,7 @@ extern "C" { DBOUT << "y_boundaries: " << y_boundaries.first << ", " << y_boundaries.second << std::endl; DBOUT << offset_y + crop_height << " < " << y_boundaries.second+y_boundaries.first <<" <= " << height << std::endl; - // if (x_boundaries.first>offset_x || x_boundaries.second+x_boundaries.firstoffset_y || y_boundaries.second+y_boundaries.first < crop_height+ offset_y){ - // std::cerr << "Invalid crop y offset" << std::endl; - // } + offset_x = x_boundaries.first; offset_y = y_boundaries.first; crop_width = x_boundaries.second; @@ -270,40 +264,24 @@ extern "C" { unsigned long dstSize = 0; // crop the input image - result = tjTransform(tj_transformer, input_buffer, input_size, - 1, &dstBuf, &dstSize, &xform, - TJFLAG_FASTDCT); - - if (result == -1) { - const char* error_message = tjGetErrorStr(); - // Handle the error - std::cerr << "Error in tjTransform: " << error_message << std::endl; - dstBuf = input_buffer; - dstSize = input_size; - } - else{ - DBOUT << "Cropped image size: " << dstSize << std::endl; - } + tj3SetCroppingRegion(tj_decompressor, xform.r); - - result = tjDecompress2(tj_decompressor, dstBuf, dstSize, tmp_buffer, - crop_width, 0, crop_height, - TJPF_RGB, TJFLAG_FASTDCT | TJFLAG_NOREALLOC); + result = tj3Decompress8(tj_decompressor, input_buffer, input_size, tmp_buffer, + 0, TJPF_RGB); if (result == -1) { - const char* error_message = tjGetErrorStr(); + const char* error_message = tj3GetErrorStr(tj_decompressor); // Handle the error - std::cerr << "Error in tjDecompress2: " << error_message << std::endl; + std::cerr << "Error in tj3Decompress8: " << error_message << std::endl; return -1; } - tjFree(dstBuf); // resize the cropped image cv::Mat source_matrix(crop_height, crop_width, CV_8UC3, (uint8_t*) tmp_buffer); cv::Mat dest_matrix(tar_height, tar_width, CV_8UC3, (uint8_t*) output_buffer); - int dx = offset_x-x_boundaries.first; - int dy = offset_y-y_boundaries.first; + // int dx = offset_x-x_boundaries.first; + // int dy = offset_y-y_boundaries.first; cv::resize((source_matrix // .colRange(dx,crop_width) diff --git a/setup.py b/setup.py index 8254870a..54a08846 100644 --- a/setup.py +++ b/setup.py @@ -121,4 +121,5 @@ def pkgconfig(package, kw): 'tqdm', 'psutil', 'numba', + 'PyTurboJPEG', ]) From d9d3e82eafe4b991a318bbfa24adc1fc071f0ab0 Mon Sep 17 00:00:00 2001 From: gent Date: Sun, 25 Feb 2024 16:05:58 +0000 Subject: [PATCH 06/31] Update dependencies and improve image encoding --- .gitignore | 2 +- README.md | 2 +- ffcv/fields/rgb_image.py | 24 ++++---- libffcv/libffcv.cpp | 115 ++++++++------------------------------- 4 files changed, 37 insertions(+), 106 deletions(-) diff --git a/.gitignore b/.gitignore index f4c53236..45c52f78 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ src __pycache__/ *.py[cod] *$py.class - +outputs/ .vscode # C extensions diff --git a/README.md b/README.md index 4d273b36..45de34bf 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Keep your training algorithm the same, just replace the data loader! Look at the ## Installation ### Linux ``` -conda create -y -n ffcv python=3.9 cupy pkg-config libjpeg-turbo opencv pytorch torchvision cudatoolkit=11.3 numba -c pytorch -c conda-forge +conda create -y -n ffcv python>=3.9 cupy pkg-config libjpeg-turbo>=3.0.0 opencv pytorch torchvision cudatoolkit=11.3 numba -c pytorch -c conda-forge conda activate ffcv pip install ffcv ``` diff --git a/ffcv/fields/rgb_image.py b/ffcv/fields/rgb_image.py index aa70a1cb..2471c45c 100644 --- a/ffcv/fields/rgb_image.py +++ b/ffcv/fields/rgb_image.py @@ -22,13 +22,10 @@ IMAGE_MODES['jpg'] = 0 IMAGE_MODES['raw'] = 1 -from turbojpeg import TurboJPEG, TJCS_RGB +from turbojpeg import TurboJPEG, TJCS_RGB, TJSAMP_444 turbo_jpeg = TurboJPEG() -def encode_jpeg(numpy_image, quality): - # numpy_image = cv2.cvtColor(numpy_image, cv2.COLOR_RGB2BGR) - # success, result = cv2.imencode('.jpg', numpy_image, - # [int(cv2.IMWRITE_JPEG_QUALITY), quality]) - result = turbo_jpeg.encode(numpy_image, quality=quality, pixel_format=TJCS_RGB,) +def encode_jpeg(numpy_image, quality,jpeg_subsample=TJSAMP_444): + result = turbo_jpeg.encode(numpy_image, quality=quality, pixel_format=TJCS_RGB,jpeg_subsample=jpeg_subsample) result = np.frombuffer(result, np.uint8) return result.reshape(-1) @@ -143,9 +140,10 @@ class ResizedCropRGBImageDecoder(SimpleRGBImageDecoder, metaclass=ABCMeta): It supports both variable and constant resolution datasets. """ - def __init__(self, output_size): + def __init__(self, output_size,interpolation): super().__init__() self.output_size = output_size + self.interpolation = interpolation def declare_state_and_memory(self, previous_state: State) -> Tuple[State, AllocationQuery]: widths = self.metadata['width'] @@ -177,7 +175,7 @@ def generate_code(self) -> Callable: scale = self.scale ratio = self.ratio - tx,ty = self.output_size + interpolation = self.interpolation if isinstance(scale, tuple): scale = np.array(scale) if isinstance(ratio, tuple): @@ -198,7 +196,7 @@ def decode(batch_indices, my_storage, metadata, storage_state): temp_buffer = temp_storage[dst_ix] imcropresizedecode_c(image_data, temp_buffer, destination[dst_ix], h,w, - i, j, cv2.INTER_CUBIC) + i, j, interpolation) else: temp_buffer = image_data.reshape(height, width, 3) resize_crop_c(temp_buffer, i, i + h, j, j + w, @@ -228,8 +226,8 @@ class RandomResizedCropRGBImageDecoder(ResizedCropRGBImageDecoder): ratio : Tuple[float] The range of potential aspect ratios that can be randomly sampled """ - def __init__(self, output_size, scale=(0.08, 1.0), ratio=(0.75, 4/3)): - super().__init__(output_size) + def __init__(self, output_size, scale=(0.08, 1.0), ratio=(0.75, 4/3), interpolation=cv2.INTER_CUBIC): + super().__init__(output_size, interpolation=interpolation) self.scale = scale self.ratio = ratio self.output_size = output_size @@ -252,8 +250,8 @@ class CenterCropRGBImageDecoder(ResizedCropRGBImageDecoder): ratio of (crop size) / (min side length) """ # output size: resize crop size -> output size - def __init__(self, output_size, ratio): - super().__init__(output_size) + def __init__(self, output_size, ratio, interpolation=cv2.INTER_AREA): + super().__init__(output_size,interpolation=interpolation) self.scale = None self.ratio = ratio diff --git a/libffcv/libffcv.cpp b/libffcv/libffcv.cpp index 40f2493f..34b18d57 100644 --- a/libffcv/libffcv.cpp +++ b/libffcv/libffcv.cpp @@ -26,10 +26,9 @@ #include // For std::pair -std::pair<__uint32_t, __uint32_t> axis_to_image_boundaries(int a, int b, int img_boundary, int mcuBlock) { +int axis_to_image_boundaries(int a, int img_boundary, int mcuBlock) { int img_b = img_boundary - (img_boundary % mcuBlock); int delta_a = a % mcuBlock; - int rb = a + b; // reduce the a to align the mcu block if (a > img_b) { a = img_b; @@ -37,16 +36,7 @@ std::pair<__uint32_t, __uint32_t> axis_to_image_boundaries(int a, int b, int img } else { a -= delta_a; } - - // the b to align the mcu block - // b = rb + (mcuBlock - rb % mcuBlock) - a; - b += delta_a; - - if ((a + b) > img_b) { - b = img_b - a; - } - - return std::make_pair(a, b); + return a; } struct Boundaries { @@ -179,14 +169,7 @@ extern "C" { ) { pthread_once(&key_once, make_keys); - - tjhandle tj_transformer; tjhandle tj_decompressor; - if ((tj_transformer = pthread_getspecific(key_tj_transformer)) == NULL) - { - tj_transformer = tj3Init(TJINIT_TRANSFORM); - pthread_setspecific(key_tj_transformer, tj_transformer); - } if ((tj_decompressor = pthread_getspecific(key_tj_decompressor)) == NULL) { tj_decompressor = tj3Init(TJINIT_DECOMPRESS); @@ -195,77 +178,29 @@ extern "C" { int result ; // get info about the cropped image - int width, height, subsamp, colorspace; - result = tjDecompressHeader3(tj_decompressor, input_buffer, input_size, &width, &height, &subsamp, &colorspace); - if (result == -1) { - const char* error_message = tjGetErrorStr(); - // Handle the error - std::cerr << "Error in info: " << error_message << std::endl; - return -1; - } - else { - DBOUT << "width: " << width << " height: " << height << " Subsamp: " << subsamp << " Colorspace: " << colorspace << std::endl; - } - - // get the boundaries of the cropped image - std::pair x_boundaries = axis_to_image_boundaries(offset_x, crop_width, width, tjMCUWidth[subsamp]); - std::pair y_boundaries = axis_to_image_boundaries(offset_y, crop_height, height, tjMCUWidth[subsamp]); - - // reduce the crop size if it is out of the image boundaries - int lbound = x_boundaries.first + x_boundaries.second; - if(lbound "; - DBOUT << "x_boundaries: " << x_boundaries.first << ", " << x_boundaries.second << std::endl; - DBOUT << offset_x + crop_width << " <= " << x_boundaries.second+x_boundaries.first <<" <= " << width << std::endl; - - DBOUT << "offset_y: " << offset_y << ", " << crop_height << ", " < "; - DBOUT << "y_boundaries: " << y_boundaries.first << ", " << y_boundaries.second << std::endl; - DBOUT << offset_y + crop_height << " < " << y_boundaries.second+y_boundaries.first <<" <= " << height << std::endl; + result = tj3DecompressHeader(tj_decompressor, input_buffer, input_size); - - offset_x = x_boundaries.first; - offset_y = y_boundaries.first; - crop_width = x_boundaries.second; - crop_height = y_boundaries.second; - - // if it is not possible to crop the image, return the original image - if (crop_width<8){ - // std::cerr << "Invalid crop width " << crop_width < Date: Wed, 28 Feb 2024 14:12:04 +0000 Subject: [PATCH 07/31] add compile pipelines for Loader --- ffcv/loader/loader.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ffcv/loader/loader.py b/ffcv/loader/loader.py index d0aa0c8a..7006a417 100644 --- a/ffcv/loader/loader.py +++ b/ffcv/loader/loader.py @@ -163,10 +163,15 @@ def __init__(self, else: raise ValueError(f"Order {order} is not a supported order type or a subclass of TraversalOrder") + self.compile_pipeline(pipelines) + memory_read = self.memory_manager.compile_reader() self.next_epoch: int = 0 - self.pipelines = {} + self.first_traversal_order = self.next_traversal_order() + + def compile_pipeline(self,pipelines): + self.pipeline_specs = {} self.field_name_to_f_ix = {} @@ -210,7 +215,6 @@ def __init__(self, memory_read) self.generate_code() - self.first_traversal_order = self.next_traversal_order() def next_traversal_order(self): return self.traversal_order.sample_order(self.next_epoch) From de4964b31815963c6d825a3328f3a40b2683857e Mon Sep 17 00:00:00 2001 From: gent Date: Sun, 3 Mar 2024 07:20:00 +0000 Subject: [PATCH 08/31] support changing pipeline --- ffcv/loader/loader.py | 3 ++- libffcv/libffcv.cpp | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/ffcv/loader/loader.py b/ffcv/loader/loader.py index 7006a417..18de1d90 100644 --- a/ffcv/loader/loader.py +++ b/ffcv/loader/loader.py @@ -165,7 +165,7 @@ def __init__(self, self.compile_pipeline(pipelines) - memory_read = self.memory_manager.compile_reader() + self.next_epoch: int = 0 self.first_traversal_order = self.next_traversal_order() @@ -210,6 +210,7 @@ def compile_pipeline(self,pipelines): if field_name not in self.pipeline_specs: self.pipeline_specs[field_name] = spec + memory_read = self.memory_manager.compile_reader() self.graph = Graph(self.pipeline_specs, self.reader.handlers, self.field_name_to_f_ix, self.reader.metadata, memory_read) diff --git a/libffcv/libffcv.cpp b/libffcv/libffcv.cpp index 34b18d57..0a9481fe 100644 --- a/libffcv/libffcv.cpp +++ b/libffcv/libffcv.cpp @@ -184,9 +184,9 @@ extern "C" { int height = tj3Get(tj_decompressor, TJPARAM_JPEGHEIGHT); int width = tj3Get(tj_decompressor, TJPARAM_JPEGWIDTH); - int MCU_block = tjMCUWidth[subsamp]; - int x_boundaries = axis_to_image_boundaries(offset_x, width, MCU_block); - int y_boundaries = axis_to_image_boundaries(offset_y, height, MCU_block); + // int MCU_block = tjMCUWidth[subsamp]; + int x_boundaries = axis_to_image_boundaries(offset_x, width, tjMCUWidth[subsamp]); + int y_boundaries = axis_to_image_boundaries(offset_y, height, tjMCUHeight[subsamp]); crop_width = offset_x + crop_width - x_boundaries; crop_height = offset_y + crop_height - y_boundaries; From 02b525bce96c07bd0657ae59de27d8ff76db8de5 Mon Sep 17 00:00:00 2001 From: gent Date: Sun, 17 Mar 2024 13:19:16 +0000 Subject: [PATCH 09/31] Support more cache manager: Shared Momory --- README.md | 178 +++------------------------ examples/profiler.py | 16 ++- ffcv/libffcv.py | 31 ++++- ffcv/loader/loader.py | 6 + ffcv/memory_managers/net_cache.py | 84 +++++++++++++ ffcv/memory_managers/shared_cache.py | 85 +++++++++++++ libffcv/libffcv.cpp | 79 ++++++++++++ 7 files changed, 305 insertions(+), 174 deletions(-) create mode 100644 ffcv/memory_managers/net_cache.py create mode 100644 ffcv/memory_managers/shared_cache.py diff --git a/README.md b/README.md index 45de34bf..801f57f4 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,21 @@ -

-Fast Forward Computer Vision: train models at a fraction of the cost with accelerated data loading! -

- +# Fast Forward Computer Vision for Pretraining +

[install] -[quickstart] -[features] +[new features] [docs] -[support slack] -[homepage] [paper] -
-Maintainers: -Guillaume Leclerc, -Andrew Ilyas and -Logan Engstrom

-`ffcv` is a drop-in data loading system that dramatically increases data throughput in model training: - -- [Train an ImageNet model](#prepackaged-computer-vision-benchmarks) -on one GPU in 35 minutes (98¢/model on AWS) -- [Train a CIFAR-10 model](https://docs.ffcv.io/ffcv_examples/cifar10.html) -on one GPU in 36 seconds (2¢/model on AWS) -- Train a `$YOUR_DATASET` model `$REALLY_FAST` (for `$WAY_LESS`) - -Keep your training algorithm the same, just replace the data loader! Look at these speedups: - - - -`ffcv` also comes prepacked with [fast, simple code](https://github.com/libffcv/imagenet-example) for [standard vision benchmarks]((https://docs.ffcv.io/benchmarks.html)): - - +This library is derived from [FFCV](https://github.com/libffcv/ffcv) to optimize the memory usage and accelerate data loading. ## Installation ### Linux ``` -conda create -y -n ffcv python>=3.9 cupy pkg-config libjpeg-turbo>=3.0.0 opencv pytorch torchvision cudatoolkit=11.3 numba -c pytorch -c conda-forge +conda create -y -n ffcv "python>=3.9" cupy pkg-config "libjpeg-turbo>=3.0.0" opencv numba -c conda-forge conda activate ffcv +conda install pytorch-cuda=11.3 torchvision -c pytorch -c nvidia pip install ffcv ``` Troubleshooting note 1: if the above commands result in a package conflict error, try running ``conda config --env --set channel_priority flexible`` in the environment and rerunning the installation command. @@ -62,73 +39,6 @@ Troubleshooting note 3: courtesy of @kschuerholt, here is a [Dockerfile](https:/ * Install cupy depending on your CUDA Toolkit version. * `pip install ffcv` -## Citation -If you use FFCV, please cite it as: - -``` -@inproceedings{leclerc2023ffcv, - author = {Guillaume Leclerc and Andrew Ilyas and Logan Engstrom and Sung Min Park and Hadi Salman and Aleksander Madry}, - title = {{FFCV}: Accelerating Training by Removing Data Bottlenecks}, - year = {2023}, - booktitle = {Computer Vision and Pattern Recognition (CVPR)}, - note = {\url{https://github.com/libffcv/ffcv/}. commit xxxxxxx} -} -``` -(Make sure to replace xxxxxxx above with the hash of the commit used!) - -## Quickstart -Accelerate *any* learning system with `ffcv`. -First, -convert your dataset into `ffcv` format (`ffcv` converts both indexed PyTorch datasets and -WebDatasets): -```python -from ffcv.writer import DatasetWriter -from ffcv.fields import RGBImageField, IntField - -# Your dataset (`torch.utils.data.Dataset`) of (image, label) pairs -my_dataset = make_my_dataset() -write_path = '/output/path/for/converted/ds.beton' - -# Pass a type for each data field -writer = DatasetWriter(write_path, { - # Tune options to optimize dataset size, throughput at train-time - 'image': RGBImageField(max_resolution=256, jpeg_quality=jpeg_quality), - 'label': IntField() -}) - -# Write dataset -writer.from_indexed_dataset(my_dataset) -``` -Then replace your old loader with the `ffcv` loader at train time (in PyTorch, -no other changes required!): -```python -from ffcv.loader import Loader, OrderOption -from ffcv.transforms import ToTensor, ToDevice, ToTorchImage, Cutout -from ffcv.fields.decoders import IntDecoder, RandomResizedCropRGBImageDecoder - -# Random resized crop -decoder = RandomResizedCropRGBImageDecoder((224, 224)) - -# Data decoding and augmentation -image_pipeline = [decoder, Cutout(), ToTensor(), ToTorchImage(), ToDevice(0)] -label_pipeline = [IntDecoder(), ToTensor(), ToDevice(0)] - -# Pipeline for each data field -pipelines = { - 'image': image_pipeline, - 'label': label_pipeline -} - -# Replaces PyTorch data loader (`torch.utils.data.Dataloader`) -loader = Loader(write_path, batch_size=bs, num_workers=num_workers, - order=OrderOption.RANDOM, pipelines=pipelines) - -# rest of training / validation proceeds identically -for epoch in range(epochs): - ... -``` -[See here](https://docs.ffcv.io/basics.html) for a more detailed guide to deploying `ffcv` for your dataset. - ## Prepackaged Computer Vision Benchmarks From gridding to benchmarking to fast research iteration, there are many reasons to want faster model training. Below we present premade codebases for training @@ -141,6 +51,7 @@ Above we plot the training time versus accuracy frontier, and the dataloading speeds, for 1-GPU ResNet-18 and 8-GPU ResNet-50 alongside a few baselines. +TODO: | Link to Config | top_1 | top_5 | # Epochs | Time (mins) | Architecture | Setup | |:---------------------------------------------------------------------------------------------------------------------------------------|--------:|--------:|-----------:|--------------:|:---------------|:---------| @@ -167,69 +78,12 @@ potential to raise the accuracy even further). You can find the training script here. ## Features - - -Computer vision or not, FFCV can help make training faster in a variety of -resource-constrained settings! -Our performance guide -has a more detailed account of the ways in which FFCV can adapt to different -performance bottlenecks. - - -- **Plug-and-play with any existing training code**: Rather than changing - aspects of model training itself, FFCV focuses on removing *data bottlenecks*, - which turn out to be a problem everywhere from neural network training to - linear regression. This means that: - - - FFCV can be introduced into any existing training code in just a few - lines of code (e.g., just swapping out the data loader and optionally the - augmentation pipeline); - - You don't have to change the model itself to make it faster (e.g., feel - free to analyze models *without* CutMix, Dropout, momentum scheduling, etc.); - - FFCV can speed up a lot more beyond just neural network training---in - fact, the more data-bottlenecked the application (e.g., linear regression, - bulk inference, etc.), the faster FFCV will make it! - - See our [Getting started](https://docs.ffcv.io/basics.html) guide, - [Example walkthroughs](https://docs.ffcv.io/examples.html), and - [Code examples](https://github.com/libffcv/ffcv/tree/main/examples) - to see how easy it is to get started! -- **Fast data processing without the pain**: FFCV automatically handles data - reading, pre-fetching, caching, and transfer between devices in an extremely - efficiently way, so that users don't have to think about it. -- **Automatically fused-and-compiled data processing**: By either using - [pre-written](https://docs.ffcv.io/api/transforms.html) FFCV transformations - or - [easily writing custom ones](https://docs.ffcv.io/ffcv_examples/custom_transforms.html), - users can - take advantage of FFCV's compilation and pipelining abilities, which will - automatically fuse and compile simple Python augmentations to machine code - using [Numba](https://numba.pydata.org), and schedule them asynchronously to avoid - loading delays. -- **Load data fast from RAM, SSD, or networked disk**: FFCV exposes - user-friendly options that can be adjusted based on the resources - available. For example, if a dataset fits into memory, FFCV can cache it - at the OS level and ensure that multiple concurrent processes all get fast - data access. Otherwise, FFCV can use fast process-level caching and will - optimize data loading to minimize the underlying number of disk reads. See - [The Bottleneck Doctor](https://docs.ffcv.io/bottleneck_doctor.html) - guide for more information. -- **Training multiple models per GPU**: Thanks to fully asynchronous - thread-based data loading, you can now interleave training multiple models on - the same GPU efficiently, without any data-loading overhead. See - [this guide](https://docs.ffcv.io/parameter_tuning.html) for more info. -- **Dedicated tools for image handling**: All the features above work are - equally applicable to all sorts of machine learning models, but FFCV also - offers some vision-specific features, such as fast JPEG encoding and decoding, - storing datasets as mixtures of raw and compressed images to trade off I/O - overhead and compute overhead, etc. See the - [Working with images](https://docs.ffcv.io/working_with_images.html) guide for - more information. - -# Contributors - -- [Guillaume Leclerc](https://github.com/GuillaumeLeclerc) -- [Logan Engstrom](http://loganengstrom.com/) -- [Andrew Ilyas](http://andrewilyas.com/) -- [Sam Park](http://sungminpark.com/) -- [Hadi Salman](http://hadisalman.com/) + +Compared to the original FFCV, this library has the following new features: + +- **crop decode**: RandomCrop and CenterCrop are now implemented to decode the crop region, which can save memory and accelerate decoding. + +- **cache strategy**: There is a potential issue that the OS cache will be swapped out. We use `FFCV_DEFAULT_CACHE_PROCESS` to control the cache process. The choices for the cache process are: + - `0`: os cache + - `1`: process cache + \ No newline at end of file diff --git a/examples/profiler.py b/examples/profiler.py index f4e500da..8db3a090 100644 --- a/examples/profiler.py +++ b/examples/profiler.py @@ -74,26 +74,25 @@ def load_one_epoch(args,loader): throughput=loader.reader.num_samples/(end-start) res['throughput'] = throughput x1,y = batch + x1 = x1.float() print("Mean: ", x1.mean().item(), "Std: ", x1.std().item()) return res - - def main(args): # pipe = ThreeAugmentPipeline() pipe = { 'image': [CenterCropRGBImageDecoder((args.img_size,args.img_size), 0.875), RandomHorizontalFlip(), ToTensor(), - ToDevice(torch.device('cuda')), - ToTorchImage(), - Convert(torch.float16), + # ToDevice(torch.device('cuda')), + # ToTorchImage(), # NormalizeImage(IMAGENET_MEAN, IMAGENET_STD, np.float16), + # Convert(torch.float16), ] } loader = Loader(args.data_path, batch_size=args.batch_size, num_workers=args.num_workers, pipelines=pipe,order=ffcv.loader.OrderOption.RANDOM, - batches_ahead=10, distributed=False,seed=0,) + batches_ahead=0, distributed=False,seed=0,) # warmup load_one_epoch(args,loader) @@ -106,10 +105,9 @@ def main(args): if __name__ == '__main__': parser = argparse.ArgumentParser(description="FFCV Profiler") parser.add_argument("-r", "--repeat", type=int, default=5, help="number of samples to record one step for profile.") - parser.add_argument("-b", "--batch_size", type=int, default=128, help="batch size") + parser.add_argument("-b", "--batch_size", type=int, default=256, help="batch size") parser.add_argument("-p", "--data_path", type=str, help="data path", required=True) - parser.add_argument("--num_workers", type=int, default=10, help="number of workers") - parser.add_argument("--cache",default=False,action="store_true",help="cache data") + parser.add_argument("--num_workers", type=int, default=60, help="number of workers") parser.add_argument("--exp", default=False, action="store_true", help="run experiments") parser.add_argument("--img_size", type=int, default=224, help="image size") parser.add_argument("--write_path", type=str, help='path to write result',default=None) diff --git a/ffcv/libffcv.py b/ffcv/libffcv.py index 79c72313..5644b4f4 100644 --- a/ffcv/libffcv.py +++ b/ffcv/libffcv.py @@ -1,8 +1,6 @@ -import ctypes -from numba import njit import numpy as np import platform -from ctypes import CDLL, c_int64, c_uint8, c_uint64, POINTER, c_void_p, c_uint32, c_bool, cdll +from ctypes import CDLL, c_int64, c_uint8, c_uint64, POINTER, c_void_p, c_uint32, c_bool, cdll, c_char_p, c_int32, create_string_buffer import ffcv._libffcv import cv2 @@ -77,3 +75,30 @@ def imcropresizedecode(source: np.ndarray, tmp: np.ndarray, dst: np.ndarray, def memcpy(source: np.ndarray, dest: np.ndarray): return ctypes_memcopy(source.ctypes.data, dest.ctypes.data, source.size*source.itemsize) + +ctypes_init_client = lib.init_client +ctypes_init_client.argtypes = [ c_char_p, c_int32] + +def init_client(url:str = b"localhost", port:int = 12345): + raise Exception("This function is not implemented. Because the multi-threading in numba will cause an error when reading the data. ") + return ctypes_init_client(url, port) + +ctypes_get_slice = lib.get_slice +ctypes_get_slice.argtypes = [c_int32, c_uint64, c_uint64, c_void_p] + +def get_slice(sockfd:int,start: int, end: int,buffer: np.ndarray): + raise Exception("This function is not implemented. Because the multi-threading in numba will cause an error when reading the data. ") + return ctypes_get_slice(sockfd, start, end, buffer.ctypes.data) + + +# ctypes_set_share_buffer = lib.set_share_buffer +# ctypes_set_share_buffer.argtypes = [c_void_p] + +# def set_share_buffer(buffer: np.ndarray): +# return ctypes_set_share_buffer(buffer.ctypes.data) + +# ctypes_get_share_buffer = lib.get_share_buffer +# ctypes_get_slice.restype = c_int + +# def get_share_buffer(): +# return np.frombuffer(ctypes_get_share_buffer(),dtype=np.uint8) \ No newline at end of file diff --git a/ffcv/loader/loader.py b/ffcv/loader/loader.py index 18de1d90..64c48287 100644 --- a/ffcv/loader/loader.py +++ b/ffcv/loader/loader.py @@ -153,6 +153,12 @@ def __init__(self, elif cache_type == 1: self.memory_manager: MemoryManager = ProcessCacheManager( self.reader) + elif cache_type == 2: + from ffcv.memory_managers.net_cache import NetCacheManager + self.memory_manager: MemoryManager = NetCacheManager(self.reader) + elif cache_type == 3: + from ffcv.memory_managers.shared_cache import SharedMemoryManager + self.memory_manager: MemoryManager = SharedMemoryManager(self.reader) else: raise ValueError("Unknown cache type. Use 0 for process cache, 1 for os cache, or 2 for no cache.") diff --git a/ffcv/memory_managers/net_cache.py b/ffcv/memory_managers/net_cache.py new file mode 100644 index 00000000..b2ddf93c --- /dev/null +++ b/ffcv/memory_managers/net_cache.py @@ -0,0 +1,84 @@ +from typing import TYPE_CHECKING + +import numpy as np +import numba as nb + +from .base import MemoryManager, MemoryContext +from ..pipeline.compiler import Compiler + +if TYPE_CHECKING: + from ..reader import Reader + +from ffcv.libffcv import init_client, get_slice +import threading +init_client=nb.njit(init_client) +class NetContext(MemoryContext): + def __init__(self, manager:MemoryManager, ): + + self.manager = manager + self.mmap = np.memmap(self.manager.reader.file_name, + 'uint8', mode='r') + + + @property + def state(self): + return (self.mmap, self.manager.ptrs, self.manager.sizes) + + def thread_state(self): + print("insert thread state ", threading.current_thread()) + sockfd: int = init_client() + buffer = np.zeros(int(self.manager.sizes.max())+1, dtype=np.uint8) + return (sockfd,buffer) + + def __enter__(self): + res = super().__enter__() + return res + + def __exit__(self, __exc_type, __exc_value, __traceback): + # Numpy doesn't have an API to close memory maps yet + # The only thing one can do is flush it be since we are not + # Writing to it it's pointless + # Moreover we want to avoid opening the memmap over and over + # anyway. + return super().__exit__(__exc_type, __exc_value, __traceback) + + +class NetCacheManager(MemoryManager): + + def __init__(self, reader: 'Reader'): + super().__init__(reader) + self.context = NetContext(self) + + def schedule_epoch(self, schedule): + return self.context + + @property + def state_type(self): + t1 = nb.uint8[::1] + t1.multable = False + t2 = nb.uint64[::1] + t1.mutable = False + return nb.types.Tuple([t1, t2, t2,nb.int32, t1]) + + def compile_reader(self): + c_get_slice = nb.njit(get_slice) + # buffer = self.context.buffer + + def read(address, mem_state): + mmap, ptrs, sizes, sockfd, buffer = mem_state + size = sizes[np.searchsorted(ptrs, address)] + if len(mem_state[0]) #include #include +#include + +#include +#include +#include +#include + #ifdef _WIN32 typedef unsigned __int32 __uint32_t; typedef unsigned __int64 __uint64_t; @@ -51,6 +58,7 @@ extern "C" { static pthread_key_t key_tj_transformer; // a key use to point to the tjdecompressor instance static pthread_key_t key_tj_decompressor; + static pthread_key_t key_share_buffer; static pthread_once_t key_once = PTHREAD_ONCE_INIT; // will make the keys to access the tj instances @@ -58,6 +66,7 @@ extern "C" { { pthread_key_create(&key_tj_decompressor, NULL); pthread_key_create(&key_tj_transformer, NULL); + pthread_key_create(&key_share_buffer, NULL); } EXPORT void resize(int64_t cresizer, int64_t source_p, int64_t sx, int64_t sy, @@ -223,4 +232,74 @@ extern "C" { dest_matrix, dest_matrix.size(), 0, 0, interpolation); return result; } + + + EXPORT int init_client(const char *url, int portno) + { + pthread_once(&key_once, make_keys); + + struct sockaddr_in serv_addr; + struct hostent *server; + + + int sockfd = socket(AF_INET, SOCK_STREAM, 0); + if (sockfd < 0) + std::cerr <<("ERROR opening socket"); + server = gethostbyname(url); + if (server == NULL) { + fprintf(stderr,"ERROR, no such host\n"); + exit(0); + } + bzero((char *) &serv_addr, sizeof(serv_addr)); + serv_addr.sin_family = AF_INET; + bcopy((char *)server->h_addr, + (char *)&serv_addr.sin_addr.s_addr, + server->h_length); + serv_addr.sin_port = htons(portno); + if (connect(sockfd,(struct sockaddr *) &serv_addr,sizeof(serv_addr)) < 0) + std::cerr <<("ERROR connecting"); + + + return sockfd; + } + + EXPORT int get_slice(int sockfd, __uint64_t start, __uint64_t end, unsigned char *buffer) + { + pthread_once(&key_once, make_keys); + struct Message { + __uint64_t start; + __uint64_t end; + __uint64_t magic = 0xdeadbeef; + }; + Message msg; + msg.start = start; + msg.end = end; + if (write(sockfd, &msg, sizeof(msg)) < 0) + { + std::cerr <<("ERROR writing to socket"); + return -1; + } + + int offset = 0; + + while (offset Date: Sun, 17 Mar 2024 16:10:38 +0000 Subject: [PATCH 10/31] Fix shared memory management and add libbuffer module --- ffcv/libffcv.py | 15 ++--------- ffcv/loader/loader.py | 5 +++- ffcv/memory_managers/shared_cache.py | 39 ++++++++++++++++------------ libffcv/libffcv.cpp | 11 -------- setup.py | 5 +++- 5 files changed, 32 insertions(+), 43 deletions(-) diff --git a/ffcv/libffcv.py b/ffcv/libffcv.py index 5644b4f4..a27378ae 100644 --- a/ffcv/libffcv.py +++ b/ffcv/libffcv.py @@ -3,6 +3,7 @@ from ctypes import CDLL, c_int64, c_uint8, c_uint64, POINTER, c_void_p, c_uint32, c_bool, cdll, c_char_p, c_int32, create_string_buffer import ffcv._libffcv import cv2 +import ctypes lib = CDLL(ffcv._libffcv.__file__) if platform.system() == "Windows": @@ -89,16 +90,4 @@ def init_client(url:str = b"localhost", port:int = 12345): def get_slice(sockfd:int,start: int, end: int,buffer: np.ndarray): raise Exception("This function is not implemented. Because the multi-threading in numba will cause an error when reading the data. ") return ctypes_get_slice(sockfd, start, end, buffer.ctypes.data) - - -# ctypes_set_share_buffer = lib.set_share_buffer -# ctypes_set_share_buffer.argtypes = [c_void_p] - -# def set_share_buffer(buffer: np.ndarray): -# return ctypes_set_share_buffer(buffer.ctypes.data) - -# ctypes_get_share_buffer = lib.get_share_buffer -# ctypes_get_slice.restype = c_int - -# def get_share_buffer(): -# return np.frombuffer(ctypes_get_share_buffer(),dtype=np.uint8) \ No newline at end of file + \ No newline at end of file diff --git a/ffcv/loader/loader.py b/ffcv/loader/loader.py index 64c48287..d1a927d4 100644 --- a/ffcv/loader/loader.py +++ b/ffcv/loader/loader.py @@ -158,7 +158,10 @@ def __init__(self, self.memory_manager: MemoryManager = NetCacheManager(self.reader) elif cache_type == 3: from ffcv.memory_managers.shared_cache import SharedMemoryManager - self.memory_manager: MemoryManager = SharedMemoryManager(self.reader) + self.memory_manager: MemoryManager = SharedMemoryManager(self.reader,'shm') + elif cache_type == 4: + from ffcv.memory_managers.shared_cache import SharedMemoryManager + self.memory_manager: MemoryManager = SharedMemoryManager(self.reader,'libbuffer') else: raise ValueError("Unknown cache type. Use 0 for process cache, 1 for os cache, or 2 for no cache.") diff --git a/ffcv/memory_managers/shared_cache.py b/ffcv/memory_managers/shared_cache.py index 9eea96a2..2e81af69 100644 --- a/ffcv/memory_managers/shared_cache.py +++ b/ffcv/memory_managers/shared_cache.py @@ -13,30 +13,35 @@ import torch.distributed as dist class SharedMemoryContext(MemoryContext): - def __init__(self, manager:MemoryManager, ): - + def __init__(self, manager:MemoryManager, type='shm'): self.manager = manager file_name = self.manager.reader.file_name name= file_name.split('/')[-1] print("loading", name) - self.mmap = np.memmap(file_name, 'uint8', mode='r') - size= len(self.mmap) - if dist.is_initialized(): - if dist.get_rank()==0: + if type=='shm': + self.mmap = np.memmap(file_name, 'uint8', mode='r') + size= len(self.mmap) + + if dist.is_initialized(): + if dist.get_rank()==0: + mem = SharedMemory(name=name, create=True, size=size) + else: + mem = SharedMemory(name=name, create=False, size=size) + else: mem = SharedMemory(name=name, create=True, size=size) + self.mem = mem + self.mmap = np.frombuffer(mem.buf, dtype=np.uint8) + if dist.is_initialized(): + if dist.get_rank()==0: + self.mmap[:] = np.fromfile(file_name, 'uint8') + dist.barrier() else: - mem = SharedMemory(name=name, create=False, size=size) - else: - mem = SharedMemory(name=name, create=True, size=size) - - self.mmap = np.frombuffer(mem.buf, dtype=np.uint8) - if dist.is_initialized(): - if dist.get_rank()==0: self.mmap[:] = np.fromfile(file_name, 'uint8') - dist.barrier() else: - self.mmap[:] = np.fromfile(file_name, 'uint8') + from ffcv import libbuffer + buffer = libbuffer.load_buffer(file_name) + self.mmap = np.frombuffer(buffer,np.uint8) @property def state(self): @@ -58,9 +63,9 @@ def __exit__(self, __exc_type, __exc_value, __traceback): class SharedMemoryManager(MemoryManager): - def __init__(self, reader: 'Reader'): + def __init__(self, reader: 'Reader', type='shm'): super().__init__(reader) - self.context = SharedMemoryContext(self) + self.context = SharedMemoryContext(self,type) def schedule_epoch(self, schedule): return self.context diff --git a/libffcv/libffcv.cpp b/libffcv/libffcv.cpp index dcfe5fd6..4a4cd6a8 100644 --- a/libffcv/libffcv.cpp +++ b/libffcv/libffcv.cpp @@ -291,15 +291,4 @@ extern "C" { return offset; } - - EXPORT int set_share_buffer(unsigned char *buffer){ - pthread_once(&key_once, make_keys); - pthread_setspecific(key_share_buffer, buffer); - return 0; - } - - EXPORT unsigned char* get_share_buffer(){ - pthread_once(&key_once, make_keys); - return pthread_getspecific(key_share_buffer); - } } diff --git a/setup.py b/setup.py index 54a08846..125e5c30 100644 --- a/setup.py +++ b/setup.py @@ -100,6 +100,9 @@ def pkgconfig(package, kw): libffcv = Extension('ffcv._libffcv', **extension_kwargs) +test_module = Extension('ffcv.libbuffer', + sources=['./libffcv/libbuffer.cpp'], + extra_compile_args=['-std=c++11']) setup(name='ffcv', version='1.1.0', @@ -111,7 +114,7 @@ def pkgconfig(package, kw): packages=find_packages(), long_description=long_description, long_description_content_type='text/markdown', - ext_modules=[libffcv], + ext_modules=[libffcv,test_module], install_requires=[ 'terminaltables', 'pytorch_pfn_extras', From 5a28c0be44b4f23aa8895688745265305e27f66e Mon Sep 17 00:00:00 2001 From: gent Date: Sun, 17 Mar 2024 16:17:47 +0000 Subject: [PATCH 11/31] Add libbuffer.cpp to the project --- libffcv/libbuffer.cpp | 70 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 libffcv/libbuffer.cpp diff --git a/libffcv/libbuffer.cpp b/libffcv/libbuffer.cpp new file mode 100644 index 00000000..e644c694 --- /dev/null +++ b/libffcv/libbuffer.cpp @@ -0,0 +1,70 @@ +#include +#include +extern "C" { + +static PyObject* create_bytes(PyObject* self) +{ + + // Create a bytes object + PyObject* py_bytes = PyBytes_FromStringAndSize("asdfs", 2); + + return py_bytes; +} +static PyObject* py_bytes = NULL; + +static PyObject* load_buffer(PyObject* self, PyObject* args){ + const char* filename; + if (!PyArg_ParseTuple(args, "s", &filename)) { + return NULL; + } + + if (py_bytes == NULL) { + FILE *f = fopen(filename, "rb"); + std::cout << "open file" << filename << std::endl; + if (!f) { + PyErr_SetString(PyExc_IOError, "Failed to open file"); + return NULL; + } + + // Get the size of the file + fseek(f, 0, SEEK_END); + long size = ftell(f); + fseek(f, 0, SEEK_SET); + + // Read the file into a buffer + unsigned char* buffer = new unsigned char[size]; + size_t read = fread(buffer, 1, size, f); + fclose(f); + + if (read != size) { + delete[] buffer; + PyErr_SetString(PyExc_IOError, "Failed to read file"); + return NULL; + } + + // Convert the buffer to a Python bytes object + py_bytes = PyBytes_FromStringAndSize((const char*) buffer, size); + }else{ + std::cout << "reuse file" << filename << std::endl; + } + + return py_bytes; +} + + +static PyMethodDef methods[] = { + {"create_bytes", (PyCFunction)create_bytes, METH_VARARGS, NULL}, + {"load_buffer", (PyCFunction)load_buffer, METH_VARARGS, NULL}, + {NULL, NULL, 0, NULL} +}; + +static struct PyModuleDef module = { + PyModuleDef_HEAD_INIT, + "libbuffer", NULL, -1, methods +}; + +PyMODINIT_FUNC PyInit_libbuffer(void) { + return PyModule_Create(&module); +} + +} // extern "C" \ No newline at end of file From 232e0d5f617d868802e8d72a003cd7227752f491 Mon Sep 17 00:00:00 2001 From: gent Date: Mon, 18 Mar 2024 04:29:30 +0000 Subject: [PATCH 12/31] v1.1.1 for shared memory --- README.md | 26 ++------- ffcv-conda.yml | 2 +- ffcv/libffcv.py | 15 ----- ffcv/loader/loader.py | 8 +-- ffcv/memory_managers/net_cache.py | 84 ---------------------------- ffcv/memory_managers/shared_cache.py | 40 ++++++------- libffcv/libbuffer.cpp | 70 ----------------------- libffcv/libffcv.cpp | 59 ------------------- setup.py | 13 +++-- 9 files changed, 31 insertions(+), 286 deletions(-) delete mode 100644 ffcv/memory_managers/net_cache.py delete mode 100644 libffcv/libbuffer.cpp diff --git a/README.md b/README.md index 801f57f4..2250a8a4 100644 --- a/README.md +++ b/README.md @@ -11,33 +11,13 @@ This library is derived from [FFCV](https://github.com/libffcv/ffcv) to optimize the memory usage and accelerate data loading. ## Installation -### Linux +### Running Environment ``` conda create -y -n ffcv "python>=3.9" cupy pkg-config "libjpeg-turbo>=3.0.0" opencv numba -c conda-forge conda activate ffcv conda install pytorch-cuda=11.3 torchvision -c pytorch -c nvidia -pip install ffcv +pip install . ``` -Troubleshooting note 1: if the above commands result in a package conflict error, try running ``conda config --env --set channel_priority flexible`` in the environment and rerunning the installation command. - -Troubleshooting note 2: on some systems (but rarely), you'll need to add the ``compilers`` package to the first command above. - -Troubleshooting note 3: courtesy of @kschuerholt, here is a [Dockerfile](https://github.com/kschuerholt/pytorch_cuda_opencv_ffcv_docker) that may help with conda-free installation - -### Windows -* Install opencv4 - * Add `..../opencv/build/x64/vc15/bin` to PATH environment variable -* Install libjpeg-turbo, download libjpeg-turbo-x.x.x-vc64.exe, not gcc64 - * Add `..../libjpeg-turbo64/bin` to PATH environment variable -* Install pthread, download last release.zip - * After unzip, rename Pre-build.2 folder to pthread - * Open `pthread/include/pthread.h`, and add the code below to the top of the file. - ```cpp - #define HAVE_STRUCT_TIMESPEC - ``` - * Add `..../pthread/dll` to PATH environment variable -* Install cupy depending on your CUDA Toolkit version. -* `pip install ffcv` ## Prepackaged Computer Vision Benchmarks From gridding to benchmarking to fast research iteration, there are many reasons @@ -86,4 +66,6 @@ Compared to the original FFCV, this library has the following new features: - **cache strategy**: There is a potential issue that the OS cache will be swapped out. We use `FFCV_DEFAULT_CACHE_PROCESS` to control the cache process. The choices for the cache process are: - `0`: os cache - `1`: process cache + - `2`: Shared Memory + - `3`: redis: not implemented yet. May be used for large-scale datasets. \ No newline at end of file diff --git a/ffcv-conda.yml b/ffcv-conda.yml index f332ea51..f3f39ca9 100644 --- a/ffcv-conda.yml +++ b/ffcv-conda.yml @@ -1,4 +1,4 @@ -name: ffcv19 +name: ffcv channels: - pytorch - defaults diff --git a/ffcv/libffcv.py b/ffcv/libffcv.py index a27378ae..3166b970 100644 --- a/ffcv/libffcv.py +++ b/ffcv/libffcv.py @@ -76,18 +76,3 @@ def imcropresizedecode(source: np.ndarray, tmp: np.ndarray, dst: np.ndarray, def memcpy(source: np.ndarray, dest: np.ndarray): return ctypes_memcopy(source.ctypes.data, dest.ctypes.data, source.size*source.itemsize) - -ctypes_init_client = lib.init_client -ctypes_init_client.argtypes = [ c_char_p, c_int32] - -def init_client(url:str = b"localhost", port:int = 12345): - raise Exception("This function is not implemented. Because the multi-threading in numba will cause an error when reading the data. ") - return ctypes_init_client(url, port) - -ctypes_get_slice = lib.get_slice -ctypes_get_slice.argtypes = [c_int32, c_uint64, c_uint64, c_void_p] - -def get_slice(sockfd:int,start: int, end: int,buffer: np.ndarray): - raise Exception("This function is not implemented. Because the multi-threading in numba will cause an error when reading the data. ") - return ctypes_get_slice(sockfd, start, end, buffer.ctypes.data) - \ No newline at end of file diff --git a/ffcv/loader/loader.py b/ffcv/loader/loader.py index d1a927d4..efb4c6c5 100644 --- a/ffcv/loader/loader.py +++ b/ffcv/loader/loader.py @@ -154,14 +154,8 @@ def __init__(self, self.memory_manager: MemoryManager = ProcessCacheManager( self.reader) elif cache_type == 2: - from ffcv.memory_managers.net_cache import NetCacheManager - self.memory_manager: MemoryManager = NetCacheManager(self.reader) - elif cache_type == 3: from ffcv.memory_managers.shared_cache import SharedMemoryManager - self.memory_manager: MemoryManager = SharedMemoryManager(self.reader,'shm') - elif cache_type == 4: - from ffcv.memory_managers.shared_cache import SharedMemoryManager - self.memory_manager: MemoryManager = SharedMemoryManager(self.reader,'libbuffer') + self.memory_manager: MemoryManager = SharedMemoryManager(self.reader) else: raise ValueError("Unknown cache type. Use 0 for process cache, 1 for os cache, or 2 for no cache.") diff --git a/ffcv/memory_managers/net_cache.py b/ffcv/memory_managers/net_cache.py deleted file mode 100644 index b2ddf93c..00000000 --- a/ffcv/memory_managers/net_cache.py +++ /dev/null @@ -1,84 +0,0 @@ -from typing import TYPE_CHECKING - -import numpy as np -import numba as nb - -from .base import MemoryManager, MemoryContext -from ..pipeline.compiler import Compiler - -if TYPE_CHECKING: - from ..reader import Reader - -from ffcv.libffcv import init_client, get_slice -import threading -init_client=nb.njit(init_client) -class NetContext(MemoryContext): - def __init__(self, manager:MemoryManager, ): - - self.manager = manager - self.mmap = np.memmap(self.manager.reader.file_name, - 'uint8', mode='r') - - - @property - def state(self): - return (self.mmap, self.manager.ptrs, self.manager.sizes) - - def thread_state(self): - print("insert thread state ", threading.current_thread()) - sockfd: int = init_client() - buffer = np.zeros(int(self.manager.sizes.max())+1, dtype=np.uint8) - return (sockfd,buffer) - - def __enter__(self): - res = super().__enter__() - return res - - def __exit__(self, __exc_type, __exc_value, __traceback): - # Numpy doesn't have an API to close memory maps yet - # The only thing one can do is flush it be since we are not - # Writing to it it's pointless - # Moreover we want to avoid opening the memmap over and over - # anyway. - return super().__exit__(__exc_type, __exc_value, __traceback) - - -class NetCacheManager(MemoryManager): - - def __init__(self, reader: 'Reader'): - super().__init__(reader) - self.context = NetContext(self) - - def schedule_epoch(self, schedule): - return self.context - - @property - def state_type(self): - t1 = nb.uint8[::1] - t1.multable = False - t2 = nb.uint64[::1] - t1.mutable = False - return nb.types.Tuple([t1, t2, t2,nb.int32, t1]) - - def compile_reader(self): - c_get_slice = nb.njit(get_slice) - # buffer = self.context.buffer - - def read(address, mem_state): - mmap, ptrs, sizes, sockfd, buffer = mem_state - size = sizes[np.searchsorted(ptrs, address)] - if len(mem_state[0]) -#include -extern "C" { - -static PyObject* create_bytes(PyObject* self) -{ - - // Create a bytes object - PyObject* py_bytes = PyBytes_FromStringAndSize("asdfs", 2); - - return py_bytes; -} -static PyObject* py_bytes = NULL; - -static PyObject* load_buffer(PyObject* self, PyObject* args){ - const char* filename; - if (!PyArg_ParseTuple(args, "s", &filename)) { - return NULL; - } - - if (py_bytes == NULL) { - FILE *f = fopen(filename, "rb"); - std::cout << "open file" << filename << std::endl; - if (!f) { - PyErr_SetString(PyExc_IOError, "Failed to open file"); - return NULL; - } - - // Get the size of the file - fseek(f, 0, SEEK_END); - long size = ftell(f); - fseek(f, 0, SEEK_SET); - - // Read the file into a buffer - unsigned char* buffer = new unsigned char[size]; - size_t read = fread(buffer, 1, size, f); - fclose(f); - - if (read != size) { - delete[] buffer; - PyErr_SetString(PyExc_IOError, "Failed to read file"); - return NULL; - } - - // Convert the buffer to a Python bytes object - py_bytes = PyBytes_FromStringAndSize((const char*) buffer, size); - }else{ - std::cout << "reuse file" << filename << std::endl; - } - - return py_bytes; -} - - -static PyMethodDef methods[] = { - {"create_bytes", (PyCFunction)create_bytes, METH_VARARGS, NULL}, - {"load_buffer", (PyCFunction)load_buffer, METH_VARARGS, NULL}, - {NULL, NULL, 0, NULL} -}; - -static struct PyModuleDef module = { - PyModuleDef_HEAD_INIT, - "libbuffer", NULL, -1, methods -}; - -PyMODINIT_FUNC PyInit_libbuffer(void) { - return PyModule_Create(&module); -} - -} // extern "C" \ No newline at end of file diff --git a/libffcv/libffcv.cpp b/libffcv/libffcv.cpp index 4a4cd6a8..a4572494 100644 --- a/libffcv/libffcv.cpp +++ b/libffcv/libffcv.cpp @@ -232,63 +232,4 @@ extern "C" { dest_matrix, dest_matrix.size(), 0, 0, interpolation); return result; } - - - EXPORT int init_client(const char *url, int portno) - { - pthread_once(&key_once, make_keys); - - struct sockaddr_in serv_addr; - struct hostent *server; - - - int sockfd = socket(AF_INET, SOCK_STREAM, 0); - if (sockfd < 0) - std::cerr <<("ERROR opening socket"); - server = gethostbyname(url); - if (server == NULL) { - fprintf(stderr,"ERROR, no such host\n"); - exit(0); - } - bzero((char *) &serv_addr, sizeof(serv_addr)); - serv_addr.sin_family = AF_INET; - bcopy((char *)server->h_addr, - (char *)&serv_addr.sin_addr.s_addr, - server->h_length); - serv_addr.sin_port = htons(portno); - if (connect(sockfd,(struct sockaddr *) &serv_addr,sizeof(serv_addr)) < 0) - std::cerr <<("ERROR connecting"); - - - return sockfd; - } - - EXPORT int get_slice(int sockfd, __uint64_t start, __uint64_t end, unsigned char *buffer) - { - pthread_once(&key_once, make_keys); - struct Message { - __uint64_t start; - __uint64_t end; - __uint64_t magic = 0xdeadbeef; - }; - Message msg; - msg.start = start; - msg.end = end; - if (write(sockfd, &msg, sizeof(msg)) < 0) - { - std::cerr <<("ERROR writing to socket"); - return -1; - } - - int offset = 0; - - while (offset Date: Tue, 19 Mar 2024 01:25:49 +0000 Subject: [PATCH 13/31] support png --- .nojekyll | 0 README.md | 3 ++- ffcv/fields/rgb_image.py | 48 ++++++++++++++++++++++++++++-------- ffcv/libffcv.py | 6 +++++ ffcv/writer.py | 45 +++++++++++++++++++++++++++++++++ libffcv/libffcv.cpp | 21 ++++++++++++++++ tests/test_image_pipeline.py | 3 +++ tests/test_webdataset.py | 2 +- 8 files changed, 116 insertions(+), 12 deletions(-) delete mode 100644 .nojekyll diff --git a/.nojekyll b/.nojekyll deleted file mode 100644 index e69de29b..00000000 diff --git a/README.md b/README.md index 2250a8a4..700f7a58 100644 --- a/README.md +++ b/README.md @@ -68,4 +68,5 @@ Compared to the original FFCV, this library has the following new features: - `1`: process cache - `2`: Shared Memory - `3`: redis: not implemented yet. May be used for large-scale datasets. - \ No newline at end of file + +- **lossless compression**: PNG is supported for lossless compression. We use `RGBImageField(mode='png')` to enable the lossless compression. diff --git a/ffcv/fields/rgb_image.py b/ffcv/fields/rgb_image.py index 2471c45c..2726a91b 100644 --- a/ffcv/fields/rgb_image.py +++ b/ffcv/fields/rgb_image.py @@ -21,6 +21,7 @@ IMAGE_MODES = Dict() IMAGE_MODES['jpg'] = 0 IMAGE_MODES['raw'] = 1 +IMAGE_MODES['png'] = 2 from turbojpeg import TurboJPEG, TJCS_RGB, TJSAMP_444 turbo_jpeg = TurboJPEG() @@ -29,6 +30,11 @@ def encode_jpeg(numpy_image, quality,jpeg_subsample=TJSAMP_444): result = np.frombuffer(result, np.uint8) return result.reshape(-1) +def encode_png(numpy_image): + x=cv2.cvtColor(numpy_image, cv2.COLOR_RGB2BGR) + result = cv2.imencode('.png', x)[1] + result = np.frombuffer(result, np.uint8) + return result.reshape(-1) def resizer(image, target_resolution): if target_resolution is None: @@ -110,9 +116,11 @@ def declare_state_and_memory(self, previous_state: State) -> Tuple[State, Alloca def generate_code(self) -> Callable: mem_read = self.memory_read imdecode_c = Compiler.compile(imdecode) + cv_imdecode_c = Compiler.compile(cv_imdecode) jpg = IMAGE_MODES['jpg'] raw = IMAGE_MODES['raw'] + png = IMAGE_MODES['png'] my_range = Compiler.get_iterator() my_memcpy = Compiler.compile(memcpy) @@ -126,8 +134,12 @@ def decode(batch_indices, destination, metadata, storage_state): if field['mode'] == jpg: imdecode_c(image_data, destination[dst_ix], height, width, height, width, 0, 0, 1, 1, False, False) - else: + elif field['mode'] == raw: my_memcpy(image_data, destination[dst_ix]) + elif field['mode'] == png: + cv_imdecode_c(image_data, destination[dst_ix]) + else: + raise ValueError(f"Unsupported mode {field['mode']}") return destination[:len(batch_indices)] @@ -165,10 +177,13 @@ def declare_state_and_memory(self, previous_state: State) -> Tuple[State, Alloca def generate_code(self) -> Callable: jpg = IMAGE_MODES['jpg'] + raw = IMAGE_MODES['raw'] + png = IMAGE_MODES['png'] mem_read = self.memory_read my_range = Compiler.get_iterator() imdecode_c = Compiler.compile(imdecode) + cv_imdecode_c = Compiler.compile(cv_imdecode) resize_crop_c = Compiler.compile(resize_crop) imcropresizedecode_c = Compiler.compile(imcropresizedecode) get_crop_c = Compiler.compile(self.get_crop_generator) @@ -191,16 +206,24 @@ def decode(batch_indices, my_storage, metadata, storage_state): width = np.uint32(field['width']) i, j, h, w = get_crop_c(height, width, scale, ratio) - + # return destination[:len(batch_indices)] if field['mode'] == jpg: temp_buffer = temp_storage[dst_ix] - imcropresizedecode_c(image_data, temp_buffer, destination[dst_ix], + res = imcropresizedecode_c(image_data, temp_buffer, destination[dst_ix], h,w, i, j, interpolation) - else: + elif field['mode'] == raw: temp_buffer = image_data.reshape(height, width, 3) resize_crop_c(temp_buffer, i, i + h, j, j + w, destination[dst_ix]) + elif field['mode'] == png: + temp_buffer = temp_storage[dst_ix] + cv_imdecode_c(image_data, temp_buffer) + buffer = temp_buffer[:height*width*3].reshape(height,width,3) + resize_crop_c(buffer, i, i + h, j, j + w, + destination[dst_ix]) + else: + raise ValueError(f"Unsupported mode {field['mode']}") return destination[:len(batch_indices)] decode.is_parallel = True @@ -330,10 +353,10 @@ def encode(self, destination, image, malloc): image = resizer(image, self.max_resolution) write_mode = self.write_mode - as_jpg = None + ccode = None # compressed code if write_mode == 'smart': - as_jpg = encode_jpeg(image, self.jpeg_quality) + ccode = encode_jpeg(image, self.jpeg_quality) write_mode = 'raw' if self.smart_threshold is not None: if image.nbytes > self.smart_threshold: @@ -343,18 +366,23 @@ def encode(self, destination, image, malloc): write_mode = 'jpg' else: write_mode = 'raw' + elif write_mode == 'png': + ccode = encode_png(image) destination['mode'] = IMAGE_MODES[write_mode] destination['height'], destination['width'] = image.shape[:2] if write_mode == 'jpg': - if as_jpg is None: - as_jpg = encode_jpeg(image, self.jpeg_quality) - destination['data_ptr'], storage = malloc(as_jpg.nbytes) - storage[:] = as_jpg + if ccode is None: + ccode = encode_jpeg(image, self.jpeg_quality) + destination['data_ptr'], storage = malloc(ccode.nbytes) + storage[:] = ccode elif write_mode == 'raw': image_bytes = np.ascontiguousarray(image).view(' Date: Thu, 4 Apr 2024 05:01:03 +0100 Subject: [PATCH 14/31] remove raise for enabling paralle --- ffcv/fields/rgb_image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffcv/fields/rgb_image.py b/ffcv/fields/rgb_image.py index 2726a91b..fed5d161 100644 --- a/ffcv/fields/rgb_image.py +++ b/ffcv/fields/rgb_image.py @@ -223,7 +223,7 @@ def decode(batch_indices, my_storage, metadata, storage_state): resize_crop_c(buffer, i, i + h, j, j + w, destination[dst_ix]) else: - raise ValueError(f"Unsupported mode {field['mode']}") + print("Unsupported mode") return destination[:len(batch_indices)] decode.is_parallel = True From 378e3b9467e4a6357aa3a814d08bd9ef49a5e830 Mon Sep 17 00:00:00 2001 From: gent Date: Thu, 4 Apr 2024 16:51:55 +0100 Subject: [PATCH 15/31] fix field mode --- ffcv/fields/rgb_image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ffcv/fields/rgb_image.py b/ffcv/fields/rgb_image.py index fed5d161..7a97480c 100644 --- a/ffcv/fields/rgb_image.py +++ b/ffcv/fields/rgb_image.py @@ -139,7 +139,7 @@ def decode(batch_indices, destination, metadata, storage_state): elif field['mode'] == png: cv_imdecode_c(image_data, destination[dst_ix]) else: - raise ValueError(f"Unsupported mode {field['mode']}") + pass return destination[:len(batch_indices)] @@ -223,7 +223,7 @@ def decode(batch_indices, my_storage, metadata, storage_state): resize_crop_c(buffer, i, i + h, j, j + w, destination[dst_ix]) else: - print("Unsupported mode") + pass return destination[:len(batch_indices)] decode.is_parallel = True From 189880a40ed47c3efc7d83cfb6996668c60d2a76 Mon Sep 17 00:00:00 2001 From: gent Date: Sun, 21 Apr 2024 00:16:17 +0100 Subject: [PATCH 16/31] fix the warn of parallel=True --- examples/profiler.py | 2 +- examples/vis_loader.py | 5 +++-- ffcv/fields/rgb_image.py | 17 +++++++---------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/examples/profiler.py b/examples/profiler.py index 8db3a090..e5a1ceea 100644 --- a/examples/profiler.py +++ b/examples/profiler.py @@ -81,7 +81,7 @@ def load_one_epoch(args,loader): def main(args): # pipe = ThreeAugmentPipeline() pipe = { - 'image': [CenterCropRGBImageDecoder((args.img_size,args.img_size), 0.875), + 'image': [CenterCropRGBImageDecoder((args.img_size,args.img_size),224/256), RandomHorizontalFlip(), ToTensor(), # ToDevice(torch.device('cuda')), diff --git a/examples/vis_loader.py b/examples/vis_loader.py index 69011e28..770f51a0 100644 --- a/examples/vis_loader.py +++ b/examples/vis_loader.py @@ -16,12 +16,13 @@ parser.add_argument('--write_path', type=str, default='viz.png', help='Path to write result') args = parser.parse_args() - loader = Loader(args.data_path, batch_size=args.batch_size, num_workers=10, cache_type=1, pipelines={ - 'image':[RandomResizedCropRGBImageDecoder((224, 224)), + loader = Loader(args.data_path, batch_size=args.batch_size, num_workers=10, cache_type=0, pipelines={ + 'image':[CenterCropRGBImageDecoder((224, 224),224/256), ToTensor(), ToTorchImage()] }, batches_ahead=0,) + print("num samples: ", loader.reader.num_samples, "fields: ", loader.reader.field_names) for x,_ in loader: x1 = x.float() print("Mean: ", x1.mean().item(), "Std: ", x1.std().item()) diff --git a/ffcv/fields/rgb_image.py b/ffcv/fields/rgb_image.py index 7a97480c..6ce10ebb 100644 --- a/ffcv/fields/rgb_image.py +++ b/ffcv/fields/rgb_image.py @@ -31,8 +31,8 @@ def encode_jpeg(numpy_image, quality,jpeg_subsample=TJSAMP_444): return result.reshape(-1) def encode_png(numpy_image): - x=cv2.cvtColor(numpy_image, cv2.COLOR_RGB2BGR) - result = cv2.imencode('.png', x)[1] + # x=cv2.cvtColor(numpy_image, cv2.COLOR_RGB2BGR) + result = cv2.imencode('.png', numpy_image)[1] result = np.frombuffer(result, np.uint8) return result.reshape(-1) @@ -43,7 +43,7 @@ def resizer(image, target_resolution): ratio = target_resolution / original_size.max() if ratio < 1: new_size = (ratio * original_size).astype(int) - image = cv2.resize(image, tuple(new_size), interpolation=cv2.INTER_AREA) + image = cv2.resize(image, tuple(new_size), interpolation=cv2.INTER_CUBIC) return image @@ -356,8 +356,7 @@ def encode(self, destination, image, malloc): ccode = None # compressed code if write_mode == 'smart': - ccode = encode_jpeg(image, self.jpeg_quality) - write_mode = 'raw' + write_mode = 'png' if self.smart_threshold is not None: if image.nbytes > self.smart_threshold: write_mode = 'jpg' @@ -365,16 +364,13 @@ def encode(self, destination, image, malloc): if np.random.rand() < self.proportion: write_mode = 'jpg' else: - write_mode = 'raw' - elif write_mode == 'png': - ccode = encode_png(image) + write_mode = 'png' destination['mode'] = IMAGE_MODES[write_mode] destination['height'], destination['width'] = image.shape[:2] if write_mode == 'jpg': - if ccode is None: - ccode = encode_jpeg(image, self.jpeg_quality) + ccode = encode_jpeg(image, self.jpeg_quality) destination['data_ptr'], storage = malloc(ccode.nbytes) storage[:] = ccode elif write_mode == 'raw': @@ -382,6 +378,7 @@ def encode(self, destination, image, malloc): destination['data_ptr'], storage = malloc(image.nbytes) storage[:] = image_bytes elif write_mode == 'png': + ccode = encode_png(image) destination['data_ptr'], storage = malloc(ccode.nbytes) storage[:] = ccode else: From 95ed7645d88b0a35f46fbca134b40f6763747438 Mon Sep 17 00:00:00 2001 From: gent Date: Thu, 9 May 2024 17:39:04 +0100 Subject: [PATCH 17/31] fix not a supported order type --- ffcv/loader/loader.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ffcv/loader/loader.py b/ffcv/loader/loader.py index efb4c6c5..ce1fc96c 100644 --- a/ffcv/loader/loader.py +++ b/ffcv/loader/loader.py @@ -117,7 +117,7 @@ def __init__(self, 'fname': fname, 'batch_size': batch_size, 'num_workers': num_workers, - 'os_cache': cache_type, + 'cache_type': cache_type, 'order': order, 'distributed': distributed, 'seed': seed, @@ -158,11 +158,13 @@ def __init__(self, self.memory_manager: MemoryManager = SharedMemoryManager(self.reader) else: raise ValueError("Unknown cache type. Use 0 for process cache, 1 for os cache, or 2 for no cache.") - + if order in ORDER_MAP: self.traversal_order: TraversalOrder = ORDER_MAP[order](self) elif isinstance(order, TraversalOrder): self.traversal_order: TraversalOrder = order(self) + elif issubclass(order, TraversalOrder): + self.traversal_order: TraversalOrder = order(self) else: raise ValueError(f"Order {order} is not a supported order type or a subclass of TraversalOrder") @@ -195,7 +197,7 @@ def compile_pipeline(self,pipelines): raise ValueError(msg) custom_pipeline_specs[output_name] = spec - # Adding the default pipelines + # Adding the default pipelines, WARN: disable default pipelines for f_ix, (field_name, field) in enumerate(self.reader.handlers.items()): self.field_name_to_f_ix[field_name] = f_ix From 3847b505f6dbda6cc5c48887e4c38a7461ee2983 Mon Sep 17 00:00:00 2001 From: gent Date: Sun, 19 May 2024 16:29:07 +0100 Subject: [PATCH 18/31] fix bugs in benchmark; reduce the memory usage for imcropdecode --- README.md | 17 +- examples/profiler.py | 18 ++- ffcv/benchmarks/decorator.py | 7 +- ffcv/benchmarks/suites/image_read.py | 12 +- ffcv/benchmarks/suites/jpeg_decode.py | 6 +- ffcv/benchmarks/suites/memory_read.py | 7 +- ffcv/fields/rgb_image.py | 33 +++- ffcv/libffcv.py | 4 +- ffcv/transforms/color_jitter.py | 220 +++++++++++++++++++++++++- libffcv/libffcv.cpp | 10 +- setup.py | 2 +- test_data/cat.JPEG | Bin 0 -> 192098 bytes 12 files changed, 297 insertions(+), 39 deletions(-) create mode 100755 test_data/cat.JPEG diff --git a/README.md b/README.md index 700f7a58..29748c86 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,20 @@ to want faster model training. Below we present premade codebases for training on ImageNet and CIFAR, including both (a) extensible codebases and (b) numerous premade training configurations. +## Make Dataset +We provide a script to make the dataset `examples/write_dataset.py`, which provides three mode: +- `jpg`: The script will compress all the images to jpg format. +- `png`: The script will compress all the images to png format. This format is too slow. +- `raw`: The script will not compress the images. +- `smart`: The script will compress the images larger than the `threshold`. +- `proportion`: The script will compress a random subset of the data with size specified by the `compress_probability` argument. + +``` +python examples/write_dataset.py --cfg.write_mode=smart --cfg.threshold=206432 --cfg.jpeg_quality=90 \ + --cfg.num_workers=40 --cfg.max_resolution=500 \ + --cfg.data_dir=$IMAGENET_DIR/train \ + --cfg.write_path=$write_path +``` ### ImageNet We provide a self-contained script for training ImageNet fast. Above we plot the training time versus @@ -67,6 +81,7 @@ Compared to the original FFCV, this library has the following new features: - `0`: os cache - `1`: process cache - `2`: Shared Memory - - `3`: redis: not implemented yet. May be used for large-scale datasets. - **lossless compression**: PNG is supported for lossless compression. We use `RGBImageField(mode='png')` to enable the lossless compression. + +- **few memory**: We optimize the memory usage and accelerate data loading. diff --git a/examples/profiler.py b/examples/profiler.py index e5a1ceea..9190d7f7 100644 --- a/examples/profiler.py +++ b/examples/profiler.py @@ -81,7 +81,7 @@ def load_one_epoch(args,loader): def main(args): # pipe = ThreeAugmentPipeline() pipe = { - 'image': [CenterCropRGBImageDecoder((args.img_size,args.img_size),224/256), + 'image': [RandomResizedCropRGBImageDecoder((args.img_size,args.img_size)), RandomHorizontalFlip(), ToTensor(), # ToDevice(torch.device('cuda')), @@ -92,8 +92,11 @@ def main(args): } loader = Loader(args.data_path, batch_size=args.batch_size, num_workers=args.num_workers, pipelines=pipe,order=ffcv.loader.OrderOption.RANDOM, - batches_ahead=0, distributed=False,seed=0,) + batches_ahead=2, distributed=False,seed=0,) + decoder = loader.pipeline_specs['image'].decoder + decoder.use_crop_decode_(args.use_ffcv) + # warmup load_one_epoch(args,loader) @@ -107,6 +110,7 @@ def main(args): parser.add_argument("-r", "--repeat", type=int, default=5, help="number of samples to record one step for profile.") parser.add_argument("-b", "--batch_size", type=int, default=256, help="batch size") parser.add_argument("-p", "--data_path", type=str, help="data path", required=True) + parser.add_argument("--use_ffcv",default=False,action="store_true") parser.add_argument("--num_workers", type=int, default=60, help="number of workers") parser.add_argument("--exp", default=False, action="store_true", help="run experiments") parser.add_argument("--img_size", type=int, default=224, help="image size") @@ -123,12 +127,12 @@ def main(args): else: data = [] with open(args.write_path,"a") as file: - for num_workers in [10,20,30,40,50,60]: - for cache in [True,False]: - for bs in [64,128,256]: + for num_workers in [10,20,40]: + for use_ffcv in [False,True]: + for bs in [128,256,512]: args.num_workers=num_workers - args.cache = cache args.batch_size = bs + args.use_ffcv=use_ffcv row = args.__dict__ for res in main(args): row.update(res) @@ -138,6 +142,4 @@ def main(args): import pandas as pd df = pd.DataFrame(data) print(df) - - exit(0) \ No newline at end of file diff --git a/ffcv/benchmarks/decorator.py b/ffcv/benchmarks/decorator.py index 4e8d75a7..48cb48ba 100644 --- a/ffcv/benchmarks/decorator.py +++ b/ffcv/benchmarks/decorator.py @@ -66,16 +66,11 @@ def run_all(runs=3, warm_up=1, pattern='*'): throughput = args['n'] / median_time unit = 'it/sec' - if throughput < 1: - unit = 'sec/it' - throughput = 1 /throughput - - throughput = np.round(throughput * 10) / 10 results[suite_name].append({ **args, 'time': median_time, - 'throughput': str(throughput) + ' ' + unit + f'throughput ({unit})': f"{throughput:.2f}" }) it_args.close() it_suite.close() diff --git a/ffcv/benchmarks/suites/image_read.py b/ffcv/benchmarks/suites/image_read.py index 89f09f46..7ce54d57 100644 --- a/ffcv/benchmarks/suites/image_read.py +++ b/ffcv/benchmarks/suites/image_read.py @@ -42,18 +42,21 @@ def __getitem__(self, index): 'length': [3000], 'mode': [ 'raw', - 'jpg' + 'jpg', + 'png', ], 'num_workers': [ 1, 8, - 16 + 16, + 32, ], 'batch_size': [ 500 ], 'size': [ (32, 32), # CIFAR + (224,224), (300, 500), # ImageNet ], 'compile': [ @@ -83,13 +86,12 @@ def __enter__(self): self.handle.__enter__() name = self.handle.name - writer = DatasetWriter(self.length, name, { + writer = DatasetWriter(name, { 'index': IntField(), 'value': RGBImageField(write_mode=self.mode) }) - with writer: - writer.write_pytorch_dataset(self.dataset, num_workers=-1, chunksize=100) + writer.from_indexed_dataset(self.dataset, chunksize=100) reader = Reader(name) manager = OSCacheManager(reader) diff --git a/ffcv/benchmarks/suites/jpeg_decode.py b/ffcv/benchmarks/suites/jpeg_decode.py index 31fc7860..51c74449 100644 --- a/ffcv/benchmarks/suites/jpeg_decode.py +++ b/ffcv/benchmarks/suites/jpeg_decode.py @@ -14,9 +14,9 @@ @benchmark({ 'n': [500], 'source_image': ['../../../test_data/pig.png'], - 'image_width': [500, 256, 1024], - 'quality': [50, 90], - 'compile': [True] + 'image_width': [224, 500, 1024], + 'quality': [50, 80, 90, 95], + 'compile': [True], }) class JPEGDecodeBenchmark(Benchmark): diff --git a/ffcv/benchmarks/suites/memory_read.py b/ffcv/benchmarks/suites/memory_read.py index e6072516..2666fa34 100644 --- a/ffcv/benchmarks/suites/memory_read.py +++ b/ffcv/benchmarks/suites/memory_read.py @@ -59,13 +59,12 @@ def __enter__(self): handle = self.handle.__enter__() name = handle.name dataset = DummyDataset(self.num_samples, self.size_bytes) - writer = DatasetWriter(self.num_samples, name, { + writer = DatasetWriter(name, { 'index': IntField(), 'value': BytesField() - }) + }, num_workers=-1) - with writer: - writer.write_pytorch_dataset(dataset, num_workers=-1, chunksize=100) + writer.from_indexed_dataset(dataset, chunksize=100) reader = Reader(name) manager = OSCacheManager(reader) diff --git a/ffcv/fields/rgb_image.py b/ffcv/fields/rgb_image.py index 6ce10ebb..270a60a9 100644 --- a/ffcv/fields/rgb_image.py +++ b/ffcv/fields/rgb_image.py @@ -156,6 +156,10 @@ def __init__(self, output_size,interpolation): super().__init__() self.output_size = output_size self.interpolation = interpolation + self.use_crop_decode = True + + def use_crop_decode_(self, value): + self.use_crop_decode = value def declare_state_and_memory(self, previous_state: State) -> Tuple[State, AllocationQuery]: widths = self.metadata['width'] @@ -165,7 +169,10 @@ def declare_state_and_memory(self, previous_state: State) -> Tuple[State, Alloca self.max_height = np.uint64(heights.max()) output_shape = (self.output_size[0], self.output_size[1], 3) my_dtype = np.dtype(' Callable: scale = self.scale ratio = self.ratio + use_crop_decode = self.use_crop_decode interpolation = self.interpolation if isinstance(scale, tuple): scale = np.array(scale) @@ -206,12 +214,23 @@ def decode(batch_indices, my_storage, metadata, storage_state): width = np.uint32(field['width']) i, j, h, w = get_crop_c(height, width, scale, ratio) - # return destination[:len(batch_indices)] + if field['mode'] == jpg: temp_buffer = temp_storage[dst_ix] - res = imcropresizedecode_c(image_data, temp_buffer, destination[dst_ix], - h,w, - i, j, interpolation) + if use_crop_decode: + imcropresizedecode_c(image_data, destination[dst_ix], + h,w, + i, j, interpolation) + else: + ## decode the whole image + imdecode_c(image_data, temp_buffer, + height, width, height, width, 0, 0, 1, 1, False, False) + ## crop and resize the image + selected_size = 3 * height * width + temp_buffer = temp_buffer.reshape(-1)[:selected_size] + temp_buffer = temp_buffer.reshape(height, width, 3) + resize_crop_c(temp_buffer, i, i + h, j, j + w, + destination[dst_ix]) elif field['mode'] == raw: temp_buffer = image_data.reshape(height, width, 3) resize_crop_c(temp_buffer, i, i + h, j, j + w, @@ -356,7 +375,7 @@ def encode(self, destination, image, malloc): ccode = None # compressed code if write_mode == 'smart': - write_mode = 'png' + write_mode = 'raw' if self.smart_threshold is not None: if image.nbytes > self.smart_threshold: write_mode = 'jpg' @@ -364,7 +383,7 @@ def encode(self, destination, image, malloc): if np.random.rand() < self.proportion: write_mode = 'jpg' else: - write_mode = 'png' + write_mode = 'raw' destination['mode'] = IMAGE_MODES[write_mode] destination['height'], destination['width'] = image.shape[:2] diff --git a/ffcv/libffcv.py b/ffcv/libffcv.py index 95944c6d..409709b9 100644 --- a/ffcv/libffcv.py +++ b/ffcv/libffcv.py @@ -59,13 +59,13 @@ def imdecode(source: np.ndarray, dst: np.ndarray, c_uint32, ] -def imcropresizedecode(source: np.ndarray, tmp: np.ndarray, dst: np.ndarray, +def imcropresizedecode(source: np.ndarray, dst: np.ndarray, crop_height: int, crop_width: int, offset_y=0, offset_x=0, interpolation=cv2.INTER_CUBIC): return ctypes_imcropresizedecode( source.ctypes.data, source.size, - tmp.ctypes.data, dst.ctypes.data, + dst.ctypes.data, dst.shape[0], dst.shape[1], crop_height, crop_width, offset_y, offset_x, diff --git a/ffcv/transforms/color_jitter.py b/ffcv/transforms/color_jitter.py index a79b72fd..3e17bfab 100644 --- a/ffcv/transforms/color_jitter.py +++ b/ffcv/transforms/color_jitter.py @@ -3,8 +3,13 @@ Reference : https://github.com/pytorch/vision/blob/main/torchvision/transforms/functional_tensor.py ''' +from typing import Callable, Optional, Tuple +import numbers +import math +import random import numpy as np - +from numba import njit +import numba as nb from dataclasses import replace from ..pipeline.allocation_query import AllocationQuery from ..pipeline.operation import Operation @@ -137,3 +142,216 @@ def blend(img1, img2, ratio): return (ratio*img1 + (1-ratio)*img2).clip(0, 255). def declare_state_and_memory(self, previous_state): return (replace(previous_state, jit_mode=True), AllocationQuery(previous_state.shape, previous_state.dtype)) + +# copy from https://github.com/facebookresearch/FFCV-SSL/blob/main/ffcv/transforms/colorjitter.py +@njit(parallel=False, fastmath=True, inline="always") +def apply_cj( + im, + apply_bri, + bri_ratio, + apply_cont, + cont_ratio, + apply_sat, + sat_ratio, + apply_hue, + hue_factor, +): + + gray = ( + np.float32(0.2989) * im[..., 0] + + np.float32(0.5870) * im[..., 1] + + np.float32(0.1140) * im[..., 2] + ) + one = np.float32(1) + # Brightness + if apply_bri: + im = im * bri_ratio + + # Contrast + if apply_cont: + im = cont_ratio * im + (one - cont_ratio) * np.float32(gray.mean()) + + # Saturation + if apply_sat: + im[..., 0] = sat_ratio * im[..., 0] + (one - sat_ratio) * gray + im[..., 1] = sat_ratio * im[..., 1] + (one - sat_ratio) * gray + im[..., 2] = sat_ratio * im[..., 2] + (one - sat_ratio) * gray + + # Hue + if apply_hue: + hue_factor_radians = hue_factor * 2.0 * np.pi + cosA = np.cos(hue_factor_radians) + sinA = np.sin(hue_factor_radians) + v1, v2, v3 = 1.0 / 3.0, np.sqrt(1.0 / 3.0), (1.0 - cosA) + hue_matrix = [ + [ + cosA + v3 / 3.0, + v1 * v3 - v2 * sinA, + v1 * v3 + v2 * sinA, + ], + [ + v1 * v3 + v2 * sinA, + cosA + v1 * v3, + v1 * v3 - v2 * sinA, + ], + [ + v1 * v3 - v2 * sinA, + v1 * v3 + v2 * sinA, + cosA + v1 * v3, + ], + ] + hue_matrix = np.array(hue_matrix, dtype=np.float64).T + for row in nb.prange(im.shape[0]): + im[row] = im[row] @ hue_matrix + return np.clip(im, 0, 255).astype(np.uint8) + + +class RandomColorJitter(Operation): + """Add ColorJitter with probability jitter_prob. + Operates on raw arrays (not tensors). + + see https://github.com/pytorch/vision/blob/28557e0cfe9113a5285330542264f03e4ba74535/torchvision/transforms/functional_tensor.py#L165 + and https://sanje2v.wordpress.com/2021/01/11/accelerating-data-transforms/ + Parameters + ---------- + jitter_prob : float, The probability with which to apply ColorJitter. + brightness (float or tuple of float (min, max)): How much to jitter brightness. + brightness_factor is chosen uniformly from [max(0, 1 - brightness), 1 + brightness] + or the given [min, max]. Should be non negative numbers. + contrast (float or tuple of float (min, max)): How much to jitter contrast. + contrast_factor is chosen uniformly from [max(0, 1 - contrast), 1 + contrast] + or the given [min, max]. Should be non negative numbers. + saturation (float or tuple of float (min, max)): How much to jitter saturation. + saturation_factor is chosen uniformly from [max(0, 1 - saturation), 1 + saturation] + or the given [min, max]. Should be non negative numbers. + hue (float or tuple of float (min, max)): How much to jitter hue. + hue_factor is chosen uniformly from [-hue, hue] or the given [min, max]. + Should have 0<= hue <= 0.5 or -0.5 <= min <= max <= 0.5. + """ + + def __init__( + self, + brightness=0.8, + contrast=0.4, + saturation=0.4, + hue=0.2, + p=0.5, + seed=None, + ): + super().__init__() + self.jitter_prob = p + + self.brightness = self._check_input(brightness, "brightness") + self.contrast = self._check_input(contrast, "contrast") + self.saturation = self._check_input(saturation, "saturation") + self.hue = self._check_input(hue, "hue", center=0, bound=(-0.5, 0.5)) + self.seed = seed + assert self.jitter_prob >= 0 and self.jitter_prob <= 1 + + def _check_input( + self, value, name, center=1, bound=(0, float("inf")), clip_first_on_zero=True + ): + if isinstance(value, numbers.Number): + if value < 0: + raise ValueError( + f"If {name} is a single number, it must be non negative." + ) + value = [center - float(value), center + float(value)] + if clip_first_on_zero: + value[0] = max(value[0], 0.0) + elif isinstance(value, (tuple, list)) and len(value) == 2: + if not bound[0] <= value[0] <= value[1] <= bound[1]: + raise ValueError(f"{name} values should be between {bound}") + else: + raise TypeError( + f"{name} should be a single number or a list/tuple with length 2." + ) + + # if value is 0 or (1., 1.) for brightness/contrast/saturation + # or (0., 0.) for hue, do nothing + if value[0] == value[1] == center: + setattr(self, f"apply_{name}", False) + else: + setattr(self, f"apply_{name}", True) + return tuple(value) + + def generate_code(self) -> Callable: + my_range = Compiler.get_iterator() + + jitter_prob = self.jitter_prob + + apply_bri = self.apply_brightness + bri = self.brightness + + apply_cont = self.apply_contrast + cont = self.contrast + + apply_sat = self.apply_saturation + sat = self.saturation + + apply_hue = self.apply_hue + hue = self.hue + + seed = self.seed + if seed is None: + + def color_jitter(images, _): + for i in my_range(images.shape[0]): + if np.random.rand() > jitter_prob: + continue + + images[i] = apply_cj( + images[i].astype("float64"), + apply_bri, + np.random.uniform(bri[0], bri[1]), + apply_cont, + np.random.uniform(cont[0], cont[1]), + apply_sat, + np.random.uniform(sat[0], sat[1]), + apply_hue, + np.random.uniform(hue[0], hue[1]), + ) + return images + + color_jitter.is_parallel = True + return color_jitter + + def color_jitter(images, _, counter): + + random.seed(seed + counter) + N = images.shape[0] + values = np.zeros(N) + bris = np.zeros(N) + conts = np.zeros(N) + sats = np.zeros(N) + hues = np.zeros(N) + for i in range(N): + values[i] = np.float32(random.uniform(0, 1)) + bris[i] = np.float32(random.uniform(bri[0], bri[1])) + conts[i] = np.float32(random.uniform(cont[0], cont[1])) + sats[i] = np.float32(random.uniform(sat[0], sat[1])) + hues[i] = np.float32(random.uniform(hue[0], hue[1])) + for i in my_range(N): + if values[i] > jitter_prob: + continue + images[i] = apply_cj( + images[i].astype("float64"), + apply_bri, + bris[i], + apply_cont, + conts[i], + apply_sat, + sats[i], + apply_hue, + hues[i], + ) + return images + + color_jitter.is_parallel = True + color_jitter.with_counter = True + return color_jitter + + def declare_state_and_memory( + self, previous_state: State + ) -> Tuple[State, Optional[AllocationQuery]]: + return (replace(previous_state, jit_mode=True), None) diff --git a/libffcv/libffcv.cpp b/libffcv/libffcv.cpp index 328993e6..23c655bd 100644 --- a/libffcv/libffcv.cpp +++ b/libffcv/libffcv.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -190,7 +191,6 @@ extern "C" { EXPORT int imcropresizedecode(unsigned char *input_buffer, __uint64_t input_size, - unsigned char *tmp_buffer, unsigned char *output_buffer, __uint32_t tar_height, __uint32_t tar_width, __uint32_t crop_height, __uint32_t crop_width, @@ -231,6 +231,13 @@ extern "C" { return -1; } // decompress the cropped image + unsigned char *tmp_buffer = NULL; + size_t buf_size=tjBufSize(crop_width, crop_height, TJPF_RGB); + tmp_buffer = (unsigned char *)malloc(buf_size); + // if(buf_size>malloc_usable_size(tmp_buffer)){ + // free(tmp_buffer); + // tmp_buffer = (unsigned char *)malloc(buf_size); + // } result = tj3Decompress8(tj_decompressor, input_buffer, input_size, tmp_buffer, 0, TJPF_RGB); @@ -251,6 +258,7 @@ extern "C" { .rowRange(dy,crop_height) ), dest_matrix, dest_matrix.size(), 0, 0, interpolation); + free(tmp_buffer); return result; } } diff --git a/setup.py b/setup.py index 75bfdba7..9440491a 100644 --- a/setup.py +++ b/setup.py @@ -106,7 +106,7 @@ def pkgconfig(package, kw): setup(name='ffcv', - version='1.1.1', + version='1.1.2', description=' FFCV: Fast Forward Computer Vision ', author='MadryLab', author_email='ffcv@mit.edu', diff --git a/test_data/cat.JPEG b/test_data/cat.JPEG new file mode 100755 index 0000000000000000000000000000000000000000..f90d4433615a718ae4b60e870bb9364e977297ca GIT binary patch literal 192098 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

=%gN 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

L3{jN~&i?DT_TSu%{{X+9 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 KVS!P}U;o*wU}>HJ literal 0 HcmV?d00001 From db675a6bf559199aa58484dff1cf9ed2a13797e0 Mon Sep 17 00:00:00 2001 From: gent Date: Sun, 19 May 2024 16:59:12 +0100 Subject: [PATCH 19/31] fix wrong attribution: use_ffcv --- examples/profiler.py | 2 +- ffcv/fields/rgb_image.py | 2 +- ffcv/libffcv.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/profiler.py b/examples/profiler.py index 9190d7f7..ec2c4b5a 100644 --- a/examples/profiler.py +++ b/examples/profiler.py @@ -95,7 +95,7 @@ def main(args): batches_ahead=2, distributed=False,seed=0,) decoder = loader.pipeline_specs['image'].decoder - decoder.use_crop_decode_(args.use_ffcv) + decoder.use_crop_decode = (args.use_ffcv) # warmup load_one_epoch(args,loader) diff --git a/ffcv/fields/rgb_image.py b/ffcv/fields/rgb_image.py index 270a60a9..a28a1390 100644 --- a/ffcv/fields/rgb_image.py +++ b/ffcv/fields/rgb_image.py @@ -169,7 +169,7 @@ def declare_state_and_memory(self, previous_state: State) -> Tuple[State, Alloca self.max_height = np.uint64(heights.max()) output_shape = (self.output_size[0], self.output_size[1], 3) my_dtype = np.dtype(' Date: Sun, 19 May 2024 18:16:44 +0100 Subject: [PATCH 20/31] log memory for benchmark --- ffcv/benchmarks/decorator.py | 11 +++++++++-- libffcv/libffcv.cpp | 9 ++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/ffcv/benchmarks/decorator.py b/ffcv/benchmarks/decorator.py index 48cb48ba..9651eab3 100644 --- a/ffcv/benchmarks/decorator.py +++ b/ffcv/benchmarks/decorator.py @@ -1,3 +1,4 @@ +import tracemalloc from itertools import product from time import time from collections import defaultdict @@ -46,6 +47,8 @@ def run_all(runs=3, warm_up=1, pattern='*'): for args in it_args: # with redirect_stderr(FakeSink()): + # Start tracing memory allocations + tracemalloc.start() if True: benchmark: Benchmark = cls(**args) with benchmark: @@ -57,7 +60,9 @@ def run_all(runs=3, warm_up=1, pattern='*'): start = time() benchmark.run() timings.append(time() - start) - + # Stop tracing memory allocations + current, peak = tracemalloc.get_traced_memory() + tracemalloc.stop() median_time = np.median(timings) throughput = None @@ -70,7 +75,9 @@ def run_all(runs=3, warm_up=1, pattern='*'): results[suite_name].append({ **args, 'time': median_time, - f'throughput ({unit})': f"{throughput:.2f}" + f'throughput ({unit})': f"{throughput:.2f}", + 'current_memory (MB)': current / 10**6, + 'peak_memory (MB)': peak / 10**6, }) it_args.close() it_suite.close() diff --git a/libffcv/libffcv.cpp b/libffcv/libffcv.cpp index 23c655bd..93a5e52e 100644 --- a/libffcv/libffcv.cpp +++ b/libffcv/libffcv.cpp @@ -232,12 +232,11 @@ extern "C" { } // decompress the cropped image unsigned char *tmp_buffer = NULL; - size_t buf_size=tjBufSize(crop_width, crop_height, TJPF_RGB); + // size_t buf_size=tjBufSize(crop_width, crop_height, TJPF_RGB); + size_t buf_size=tj3JPEGBufSize(width, height, TJPF_RGB); + tmp_buffer = (unsigned char *)malloc(buf_size); - // if(buf_size>malloc_usable_size(tmp_buffer)){ - // free(tmp_buffer); - // tmp_buffer = (unsigned char *)malloc(buf_size); - // } + result = tj3Decompress8(tj_decompressor, input_buffer, input_size, tmp_buffer, 0, TJPF_RGB); From 436242be10186e35591f35f01cb2442abe035295 Mon Sep 17 00:00:00 2001 From: gent Date: Mon, 20 May 2024 09:15:43 +0100 Subject: [PATCH 21/31] feat: Add script to write dataset with configurable options --- examples/write_dataset.py | 97 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 examples/write_dataset.py diff --git a/examples/write_dataset.py b/examples/write_dataset.py new file mode 100644 index 00000000..1b077567 --- /dev/null +++ b/examples/write_dataset.py @@ -0,0 +1,97 @@ +"""example usage: +export IMAGENET_DIR=/path/to/pytorch/format/imagenet/directory/ +export WRITE_DIR=/your/path/here/ +write_dataset train 500 0.50 90 +write_path=$WRITE_DIR/train500_0.5_90.ffcv +echo "Writing ImageNet train dataset to ${write_path}" +python examples/write_dataset.py \ + --cfg.data_dir=$IMAGENET_DIR \ + --cfg.write_path=$write_path \ + --cfg.max_resolution=500 \ + --cfg.write_mode=smart \ + --cfg.compress_probability=0.50 \ + --cfg.jpeg_quality=90 +""" +from PIL import Image +from torch.utils.data import Subset +from ffcv.writer import DatasetWriter +from ffcv.fields import IntField, RGBImageField +from torchvision.datasets import ImageFolder + +from argparse import ArgumentParser +from fastargs import Section, Param +from fastargs.validation import And, OneOf +from fastargs.decorators import param, section +from fastargs import get_current_config +import cv2 +import numpy as np + +# hack resizer +# def resizer(image, target_resolution): +# if target_resolution is None: +# return image +# original_size = np.array([image.shape[1], image.shape[0]]) +# ratio = target_resolution / original_size.min() +# if ratio < 1: +# new_size = (ratio * original_size).astype(int) +# image = cv2.resize(image, tuple(new_size), interpolation=cv2.INTER_AREA) +# return image +# from ffcv.fields import rgb_image +# rgb_image.resizer = resizer + +Section('cfg', 'arguments to give the writer').params( + dataset=Param(And(str, OneOf(['cifar', 'imagenet'])), 'Which dataset to write', default='imagenet'), + data_dir=Param(str, 'Where to find the PyTorch dataset', required=True), + write_path=Param(str, 'Where to write the new dataset', required=True), + write_mode=Param(str, 'Mode: raw, smart or jpg', required=False, default='smart'), + max_resolution=Param(int, 'Max image side length. 0 any size.', required=False,default=0), + num_workers=Param(int, 'Number of workers to use', default=16), + chunk_size=Param(int, 'Chunk size for writing', default=100), + jpeg_quality=Param(float, 'Quality of jpeg images', default=90), + subset=Param(int, 'How many images to use (-1 for all)', default=-1), + compress_probability=Param(float, 'compress probability', default=0.5), + threshold=Param(int, 'threshold for smart mode to compress by jpeg', default=286432), +) + +@section('cfg') +@param('dataset') +@param('data_dir') +@param('write_path') +@param('max_resolution') +@param('num_workers') +@param('chunk_size') +@param('subset') +@param('jpeg_quality') +@param('write_mode') +@param('compress_probability') +@param('threshold') +def main(dataset, data_dir, write_path, max_resolution, num_workers, + chunk_size, subset, jpeg_quality, write_mode, + compress_probability, threshold): + my_dataset = ImageFolder(root=data_dir) + + if subset > 0: my_dataset = Subset(my_dataset, range(subset)) + writer = DatasetWriter(write_path, { + 'image': RGBImageField(write_mode=write_mode, + max_resolution=None if max_resolution==0 else max_resolution, + compress_probability=compress_probability, + jpeg_quality=jpeg_quality, + smart_threshold=threshold), + 'label': IntField(), + }, num_workers=num_workers) + + writer.from_indexed_dataset(my_dataset, chunksize=chunk_size,shuffle_indices=False) + +if __name__ == '__main__': + config = get_current_config() + parser = ArgumentParser() + config.augment_argparse(parser) + config.collect_argparse_args(parser) + config.validate(mode='stderr') + config.summary() + + args=config.get().cfg + assert args.write_path.endswith('.ffcv'), 'write_path must end with .ffcv' + file=open(args.write_path.replace(".ffcv",".meta"), 'w') + file.write(str(args.__dict__)) + main() From 2592e256f1631ca68987677ef25d01f4c2a03e49 Mon Sep 17 00:00:00 2001 From: gent Date: Tue, 21 May 2024 13:47:57 +0100 Subject: [PATCH 22/31] Refactor shared memory initialization in SharedMemoryContext --- ffcv/memory_managers/shared_cache.py | 33 +++++++++++++++++----------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/ffcv/memory_managers/shared_cache.py b/ffcv/memory_managers/shared_cache.py index 57015b76..a14efbc2 100644 --- a/ffcv/memory_managers/shared_cache.py +++ b/ffcv/memory_managers/shared_cache.py @@ -13,31 +13,38 @@ import torch.distributed as dist class SharedMemoryContext(MemoryContext): + cache_dict = {} def __init__(self, manager:MemoryManager): self.manager = manager file_name = self.manager.reader.file_name name= file_name.split('/')[-1] - print("loading", name) - - self.mmap = np.memmap(file_name, 'uint8', mode='r') - size= len(self.mmap) + mmap = np.memmap(file_name, 'uint8', mode='r') + size= len(mmap) + + def _initiate_shared_memory(create): + print("initialize shared memory for ", name) + mem = SharedMemory(name=name, create=create, size=size) + shared_mmap = np.frombuffer(mem.buf, dtype=np.uint8) + if create: + shared_mmap[:] = mmap[:] + SharedMemoryContext.cache_dict[name] = mem + return shared_mmap + if dist.is_initialized(): if dist.get_rank()==0: - mem = SharedMemory(name=name, create=True, size=size) + if name in SharedMemoryContext.cache_dict: + self.mmap = _initiate_shared_memory(False) + else: + self.mmap = _initiate_shared_memory(True) else: - mem = SharedMemory(name=name, create=False, size=size) + self.mmap = _initiate_shared_memory(False) else: - mem = SharedMemory(name=name, create=True, size=size) - self.mem = mem - self.mmap = np.frombuffer(mem.buf, dtype=np.uint8) + self.mmap = _initiate_shared_memory(True) + if dist.is_initialized(): - if dist.get_rank()==0: - self.mmap[:] = np.fromfile(file_name, 'uint8') dist.barrier() - else: - self.mmap[:] = np.fromfile(file_name, 'uint8') @property def state(self): From 77b5cb3b7cbeda7d144f7a09b10a26369ece4650 Mon Sep 17 00:00:00 2001 From: gent Date: Fri, 7 Jun 2024 12:35:41 +0100 Subject: [PATCH 23/31] fix cache --- examples/profiler.py | 17 +++++++---- examples/write_dataset.py | 11 +++++++- ffcv/memory_managers/shared_cache.py | 42 +++++++++++++++------------- 3 files changed, 44 insertions(+), 26 deletions(-) diff --git a/examples/profiler.py b/examples/profiler.py index ec2c4b5a..ff361279 100644 --- a/examples/profiler.py +++ b/examples/profiler.py @@ -5,6 +5,7 @@ from ffcv.fields.rgb_image import * from ffcv.transforms import RandomHorizontalFlip, NormalizeImage, ToTensor, ToTorchImage, ToDevice import numpy as np +import torchvision from ffcv import Loader import ffcv @@ -67,13 +68,16 @@ def summary(self): def load_one_epoch(args,loader): start = time.time() l=ramqdm(loader) - for batch in l: - pass + + for x1,y in l: + x_std = x1.float().flatten(1).std(1) + if x_std.min() <= 0: + torchvision.utils.save_image(x1.float(), "invalid_sample.png", normalize=True) + assert x_std.min() > 0, "invalid sample" end = time.time() res = l.summary() throughput=loader.reader.num_samples/(end-start) res['throughput'] = throughput - x1,y = batch x1 = x1.float() print("Mean: ", x1.mean().item(), "Std: ", x1.std().item()) return res @@ -91,8 +95,8 @@ def main(args): ] } loader = Loader(args.data_path, batch_size=args.batch_size, num_workers=args.num_workers, - pipelines=pipe,order=ffcv.loader.OrderOption.RANDOM, - batches_ahead=2, distributed=False,seed=0,) + pipelines=pipe, + batches_ahead=2, distributed=False,seed=0,drop_last=True) decoder = loader.pipeline_specs['image'].decoder decoder.use_crop_decode = (args.use_ffcv) @@ -108,7 +112,7 @@ def main(args): if __name__ == '__main__': parser = argparse.ArgumentParser(description="FFCV Profiler") parser.add_argument("-r", "--repeat", type=int, default=5, help="number of samples to record one step for profile.") - parser.add_argument("-b", "--batch_size", type=int, default=256, help="batch size") + parser.add_argument("-b", "--batch_size", type=int, default=64, help="batch size") parser.add_argument("-p", "--data_path", type=str, help="data path", required=True) parser.add_argument("--use_ffcv",default=False,action="store_true") parser.add_argument("--num_workers", type=int, default=60, help="number of workers") @@ -137,6 +141,7 @@ def main(args): for res in main(args): row.update(res) file.write(json.dumps(row)+"\n") + file.flush() print(row) data.append(row) import pandas as pd diff --git a/examples/write_dataset.py b/examples/write_dataset.py index 1b077567..a9b38bef 100644 --- a/examples/write_dataset.py +++ b/examples/write_dataset.py @@ -16,7 +16,9 @@ from torch.utils.data import Subset from ffcv.writer import DatasetWriter from ffcv.fields import IntField, RGBImageField +import torchvision from torchvision.datasets import ImageFolder +import torchvision.datasets as torch_datasets from argparse import ArgumentParser from fastargs import Section, Param @@ -68,7 +70,14 @@ def main(dataset, data_dir, write_path, max_resolution, num_workers, chunk_size, subset, jpeg_quality, write_mode, compress_probability, threshold): - my_dataset = ImageFolder(root=data_dir) + if dataset == 'imagenet': + my_dataset = ImageFolder(root=data_dir) + elif dataset == 'cifar': + tfms = torchvision.transforms.Compose([torchvision.transforms.ToTensor()]) + my_dataset = torch_datasets.CIFAR10(root=data_dir, train=True, download=True) + else: + raise ValueError('Unknown dataset') + if subset > 0: my_dataset = Subset(my_dataset, range(subset)) writer = DatasetWriter(write_path, { diff --git a/ffcv/memory_managers/shared_cache.py b/ffcv/memory_managers/shared_cache.py index a14efbc2..6f622c72 100644 --- a/ffcv/memory_managers/shared_cache.py +++ b/ffcv/memory_managers/shared_cache.py @@ -1,3 +1,5 @@ +import filecmp +import os from typing import TYPE_CHECKING import numpy as np @@ -21,27 +23,29 @@ def __init__(self, manager:MemoryManager): mmap = np.memmap(file_name, 'uint8', mode='r') size= len(mmap) + rank = dist.get_rank() - def _initiate_shared_memory(create): - print("initialize shared memory for ", name) - mem = SharedMemory(name=name, create=create, size=size) - shared_mmap = np.frombuffer(mem.buf, dtype=np.uint8) - if create: + print(f"initialize shared memory for {name} in rank {rank}") + file = os.path.join('/dev/shm',name) + create = False if os.path.exists(file) else True + self.mem = SharedMemory(name=name, create=create, size=size) + shared_mmap = np.frombuffer(self.mem.buf, dtype=np.uint8) + if create: + result = filecmp.cmp(file, file_name) + if not result: + print("copying file to shared memory") shared_mmap[:] = mmap[:] - SharedMemoryContext.cache_dict[name] = mem - return shared_mmap - - - if dist.is_initialized(): - if dist.get_rank()==0: - if name in SharedMemoryContext.cache_dict: - self.mmap = _initiate_shared_memory(False) - else: - self.mmap = _initiate_shared_memory(True) - else: - self.mmap = _initiate_shared_memory(False) - else: - self.mmap = _initiate_shared_memory(True) + self.mmap = shared_mmap + # if dist.is_initialized(): + # if dist.get_rank()==0: + # if name in SharedMemoryContext.cache_dict: + # self.mmap = _initiate_shared_memory(False) + # else: + # self.mmap = _initiate_shared_memory(True) + # else: + # self.mmap = _initiate_shared_memory(False) + # else: + # self.mmap = _initiate_shared_memory(True) if dist.is_initialized(): dist.barrier() From d6a061bb1bd907f3a1accd012c03ef58c1bdacb6 Mon Sep 17 00:00:00 2001 From: gent Date: Thu, 13 Jun 2024 12:06:47 +0100 Subject: [PATCH 24/31] better print for shared_cache --- examples/profiler.py | 4 ++-- ffcv/memory_managers/shared_cache.py | 34 +++++++++++++--------------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/examples/profiler.py b/examples/profiler.py index ff361279..43fade10 100644 --- a/examples/profiler.py +++ b/examples/profiler.py @@ -89,7 +89,7 @@ def main(args): RandomHorizontalFlip(), ToTensor(), # ToDevice(torch.device('cuda')), - # ToTorchImage(), + ToTorchImage(), # NormalizeImage(IMAGENET_MEAN, IMAGENET_STD, np.float16), # Convert(torch.float16), ] @@ -115,7 +115,7 @@ def main(args): parser.add_argument("-b", "--batch_size", type=int, default=64, help="batch size") parser.add_argument("-p", "--data_path", type=str, help="data path", required=True) parser.add_argument("--use_ffcv",default=False,action="store_true") - parser.add_argument("--num_workers", type=int, default=60, help="number of workers") + parser.add_argument("--num_workers", type=int, default=10, help="number of workers") parser.add_argument("--exp", default=False, action="store_true", help="run experiments") parser.add_argument("--img_size", type=int, default=224, help="image size") parser.add_argument("--write_path", type=str, help='path to write result',default=None) diff --git a/ffcv/memory_managers/shared_cache.py b/ffcv/memory_managers/shared_cache.py index 6f622c72..31559e9c 100644 --- a/ffcv/memory_managers/shared_cache.py +++ b/ffcv/memory_managers/shared_cache.py @@ -14,6 +14,14 @@ from multiprocessing.shared_memory import SharedMemory import torch.distributed as dist +class MasterSharedMemory(SharedMemory): + def __del__(self) -> None: + if dist.is_initialized() and dist.get_rank() == 0: + print("deleting shared memory") + super().__del__() + else: + super().__del__() + class SharedMemoryContext(MemoryContext): cache_dict = {} def __init__(self, manager:MemoryManager): @@ -23,32 +31,22 @@ def __init__(self, manager:MemoryManager): mmap = np.memmap(file_name, 'uint8', mode='r') size= len(mmap) - rank = dist.get_rank() - - print(f"initialize shared memory for {name} in rank {rank}") + rank = dist.get_rank() if dist.is_initialized() else 0 + print_args = {'force':True} if dist.is_initialized() else {} + print(f"[rank {rank}] initialize shared memory for {name}",**print_args) file = os.path.join('/dev/shm',name) create = False if os.path.exists(file) else True - self.mem = SharedMemory(name=name, create=create, size=size) + self.mem = MasterSharedMemory(name=name, create=create, size=size) shared_mmap = np.frombuffer(self.mem.buf, dtype=np.uint8) - if create: + if rank == True: result = filecmp.cmp(file, file_name) if not result: - print("copying file to shared memory") + print(f"[rank {rank}] copying file to shared memory",**print_args) shared_mmap[:] = mmap[:] - self.mmap = shared_mmap - # if dist.is_initialized(): - # if dist.get_rank()==0: - # if name in SharedMemoryContext.cache_dict: - # self.mmap = _initiate_shared_memory(False) - # else: - # self.mmap = _initiate_shared_memory(True) - # else: - # self.mmap = _initiate_shared_memory(False) - # else: - # self.mmap = _initiate_shared_memory(True) - + if dist.is_initialized(): dist.barrier() + self.mmap = shared_mmap @property def state(self): From 54ab9e06b2f0118174b19bcaf135b917fca1d2cf Mon Sep 17 00:00:00 2001 From: gent Date: Fri, 14 Jun 2024 17:41:54 +0100 Subject: [PATCH 25/31] use float32 for color jitter --- ffcv/transforms/color_jitter.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/ffcv/transforms/color_jitter.py b/ffcv/transforms/color_jitter.py index 3e17bfab..9270e805 100644 --- a/ffcv/transforms/color_jitter.py +++ b/ffcv/transforms/color_jitter.py @@ -150,12 +150,13 @@ def apply_cj( apply_bri, bri_ratio, apply_cont, - cont_ratio, + cont_ratio:np.float32, apply_sat, - sat_ratio, + sat_ratio:np.float32, apply_hue, - hue_factor, + hue_factor:np.float32, ): + gray = ( np.float32(0.2989) * im[..., 0] @@ -200,7 +201,7 @@ def apply_cj( cosA + v1 * v3, ], ] - hue_matrix = np.array(hue_matrix, dtype=np.float64).T + hue_matrix = np.array(hue_matrix, dtype=np.float32).T for row in nb.prange(im.shape[0]): im[row] = im[row] @ hue_matrix return np.clip(im, 0, 255).astype(np.uint8) @@ -301,15 +302,15 @@ def color_jitter(images, _): continue images[i] = apply_cj( - images[i].astype("float64"), + images[i].astype("float32"), apply_bri, - np.random.uniform(bri[0], bri[1]), + np.float32(np.random.uniform(bri[0], bri[1])), apply_cont, - np.random.uniform(cont[0], cont[1]), + np.float32(np.random.uniform(cont[0], cont[1])), apply_sat, - np.random.uniform(sat[0], sat[1]), + np.float32(np.random.uniform(sat[0], sat[1])), apply_hue, - np.random.uniform(hue[0], hue[1]), + np.float32(np.random.uniform(hue[0], hue[1])), ) return images @@ -335,7 +336,7 @@ def color_jitter(images, _, counter): if values[i] > jitter_prob: continue images[i] = apply_cj( - images[i].astype("float64"), + images[i].astype("float32"), apply_bri, bris[i], apply_cont, From 26c06d291d6bb1e12eab7cf624a376b128feb7c2 Mon Sep 17 00:00:00 2001 From: gent Date: Sat, 15 Jun 2024 16:11:04 +0100 Subject: [PATCH 26/31] add transformations --- ffcv/transforms/gaussian_blur.py | 91 ++++++++++++++++++++++ ffcv/transforms/grayscale.py | 125 +++++++++++++++++++++++++++++++ ffcv/transforms/solarization.py | 119 +++++++++++++++++++++++++++++ 3 files changed, 335 insertions(+) create mode 100644 ffcv/transforms/gaussian_blur.py create mode 100644 ffcv/transforms/grayscale.py create mode 100644 ffcv/transforms/solarization.py diff --git a/ffcv/transforms/gaussian_blur.py b/ffcv/transforms/gaussian_blur.py new file mode 100644 index 00000000..9aeaf30e --- /dev/null +++ b/ffcv/transforms/gaussian_blur.py @@ -0,0 +1,91 @@ +# copy from https://github.com/facebookresearch/FFCV-SSL/blob/main/ffcv/transforms/gaussian_blur.py +import numpy as np +from typing import Callable, Optional, Tuple +from dataclasses import replace +from ffcv.pipeline.allocation_query import AllocationQuery +from ffcv.pipeline.operation import Operation +from ffcv.pipeline.state import State +from ffcv.pipeline.compiler import Compiler +from scipy.signal import convolve2d + + +def apply_blur(img, kernel_size, w): + pad = (kernel_size - 1) // 2 + H, W, _ = img.shape + tmp = np.zeros(img.shape, dtype=np.float32) + for k in range(kernel_size): + start = max(0, pad - k) + stop = min(W, pad - k + W) + window = (img[:, start:stop] / 255) * w[k] + tmp[:, np.abs(stop - W) : W - start] += window + tmp2 = tmp + 0.0 + for k in range(kernel_size): + start = max(0, pad - k) + stop = min(H, pad - k + H) + window = (tmp[start:stop] * w[k]).astype(np.uint8) + tmp2[np.abs(stop - H) : H - start] += window + return np.clip(tmp2 * 255.0, 0, 255).astype(np.uint8) + + +class GaussianBlur(Operation): + """Blurs image with randomly chosen Gaussian blur. + If the image is torch Tensor, it is expected + to have [..., C, H, W] shape, where ... means an arbitrary number of leading dimensions. + + Args: + blur_prob (float): probability to apply blurring to each input + kernel_size (int or sequence): Size of the Gaussian kernel. + sigma (float or tuple of float (min, max)): Standard deviation to be used for + creating kernel to perform blurring. If float, sigma is fixed. If it is tuple + of float (min, max), sigma is chosen uniformly at random to lie in the + given range. + """ + + def __init__(self, kernel_size=5, sigma=(0.1, 2.0), p=0.5): + super().__init__() + self.blur_prob = p + self.kernel_size = kernel_size + assert sigma[1] > sigma[0] + self.sigmas = np.linspace(sigma[0], sigma[1], 10) + from scipy import signal + + self.weights = np.stack( + [ + signal.gaussian(kernel_size, s) + for s in np.linspace(sigma[0], sigma[1], 10) + ] + ) + self.weights /= self.weights.sum(1, keepdims=True) + + def generate_code(self) -> Callable: + my_range = Compiler.get_iterator() + blur_prob = self.blur_prob + kernel_size = self.kernel_size + weights = self.weights + apply_blur_c = Compiler.compile(apply_blur) + + def blur(images, indices): + + for i in my_range(images.shape[0]): + if np.random.rand() < blur_prob: + k = np.random.randint(low=0, high=10) + for ch in range(images.shape[-1]): + images[i, ..., ch] = convolve2d( + images[i, ..., ch], + np.outer(weights[k], weights[k]), + mode="same", + ) + # images[i] = apply_blur_c(images[i], kernel_size, weights[k]) + return images + + blur.is_parallel = True + blur.with_indices = True + return blur + + def declare_state_and_memory( + self, previous_state: State + ) -> Tuple[State, Optional[AllocationQuery]]: + return ( + replace(previous_state, jit_mode=False), + None, + ) diff --git a/ffcv/transforms/grayscale.py b/ffcv/transforms/grayscale.py new file mode 100644 index 00000000..e98823a1 --- /dev/null +++ b/ffcv/transforms/grayscale.py @@ -0,0 +1,125 @@ +""" +# copy from https://github.com/facebookresearch/FFCV-SSL/blob/main/ffcv/transforms/grayscale.py +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. +""" + + +from typing import Callable, Optional, Tuple +from ffcv.pipeline.allocation_query import AllocationQuery +from ffcv.pipeline.operation import Operation +from ffcv.pipeline.state import State +from ffcv.pipeline.compiler import Compiler +from dataclasses import replace +import numpy as np +import random + + +class RandomGrayscale(Operation): + """Add Gaussian Blur with probability blur_prob. + Operates on raw arrays (not tensors). + + Parameters + ---------- + blur_prob : float + The probability with which to flip each image in the batch + horizontally. + """ + + def __init__(self, p: float = 0.2, seed: int = None): + super().__init__() + self.gray_prob = p + self.seed = seed + + def generate_code(self) -> Callable: + my_range = Compiler.get_iterator() + gray_prob = self.gray_prob + seed = self.seed + + if seed is None: + + def grayscale(images, _): + for i in my_range(images.shape[0]): + if np.random.rand() > gray_prob: + continue + images[i] = ( + 0.2989 * images[i, ..., 0:1] + + 0.5870 * images[i, ..., 1:2] + + 0.1140 * images[i, ..., 2:3] + ) + return images + + grayscale.is_parallel = True + return grayscale + + def grayscale(images, _, counter): + random.seed(seed + counter) + values = np.zeros(images.shape[0]) + for i in range(images.shape[0]): + values[i] = random.uniform(0, 1) + for i in my_range(images.shape[0]): + if values[i] > gray_prob: + continue + images[i] = ( + 0.2989 * images[i, ..., 0:1] + + 0.5870 * images[i, ..., 1:2] + + 0.1140 * images[i, ..., 2:3] + ) + return images + + grayscale.with_counter = True + grayscale.is_parallel = True + return grayscale + + def declare_state_and_memory( + self, previous_state: State + ) -> Tuple[State, Optional[AllocationQuery]]: + assert previous_state.jit_mode + return (previous_state, None) + + +class LabelGrayscale(Operation): + """ColorJitter info added to the labels. Should be initialized in exactly the same way as + :cla:`ffcv.transforms.ColorJitter`. + """ + + def __init__(self, gray_prob: float = 0.2, seed: int = None): + super().__init__() + self.gray_prob = gray_prob + self.seed = np.random.RandomState(seed).randint(0, 2**32 - 1) + + def generate_code(self) -> Callable: + my_range = Compiler.get_iterator() + gray_prob = self.gray_prob + seed = self.seed + + def grayscale(labels, temp_array, indices): + rep = "" + for i in indices: + rep += str(i) + local_seed = (hash(rep) + seed) % 2**31 + temp_array[:, :-1] = labels + for i in my_range(temp_array.shape[0]): + np.random.seed(local_seed + i) + if np.random.rand() < gray_prob: + temp_array[i, -1] = 0.0 + else: + temp_array[i, -1] = 1.0 + return temp_array + + grayscale.is_parallel = True + grayscale.with_indices = True + + return grayscale + + def declare_state_and_memory( + self, previous_state: State + ) -> Tuple[State, Optional[AllocationQuery]]: + previous_shape = previous_state.shape + new_shape = (previous_shape[0] + 1,) + return ( + replace(previous_state, shape=new_shape, dtype=np.float32), + AllocationQuery(new_shape, dtype=np.float32), + ) \ No newline at end of file diff --git a/ffcv/transforms/solarization.py b/ffcv/transforms/solarization.py new file mode 100644 index 00000000..25719536 --- /dev/null +++ b/ffcv/transforms/solarization.py @@ -0,0 +1,119 @@ +# copy from https://github.com/facebookresearch/FFCV-SSL/blob/main/ffcv/transforms/solarization.py +""" +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. +""" + + +from typing import Callable, Optional, Tuple +from ffcv.pipeline.allocation_query import AllocationQuery +from ffcv.pipeline.operation import Operation +from ffcv.pipeline.state import State +from ffcv.pipeline.compiler import Compiler +import numpy as np +from dataclasses import replace +import random + + +class RandomSolarization(Operation): + """Solarize the image randomly with a given probability by inverting all pixel + values above a threshold. If img is a Tensor, it is expected to be in [..., 1 or 3, H, W] format, + where ... means it can have an arbitrary number of leading dimensions. + If img is PIL Image, it is expected to be in mode "L" or "RGB". + Parameters + ---------- + solarization_prob (float): probability of the image being solarized. Default value is 0.5 + threshold (float): all pixels equal or above this value are inverted. + """ + + def __init__( + self, threshold: float = 128, p: float = 0.5, seed: int = None + ): + super().__init__() + self.sol_prob = p + self.threshold = threshold + self.seed = seed + + def generate_code(self) -> Callable: + my_range = Compiler.get_iterator() + sol_prob = self.sol_prob + threshold = self.threshold + seed = self.seed + + if seed is None: + + def solarize(images, _): + for i in my_range(images.shape[0]): + if np.random.rand() < sol_prob: + mask = images[i] >= threshold + images[i] = np.where(mask, 255 - images[i], images[i]) + return images + + solarize.is_parallel = True + return solarize + + def solarize(images, _, counter): + random.seed(seed + counter) + values = np.zeros(len(images)) + for i in range(len(images)): + values[i] = random.uniform(0, 1) + for i in my_range(images.shape[0]): + if values[i] < sol_prob: + mask = images[i] >= threshold + images[i] = np.where(mask, 255 - images[i], images[i]) + return images + + solarize.with_counter = True + solarize.is_parallel = True + return solarize + + def declare_state_and_memory( + self, previous_state: State + ) -> Tuple[State, Optional[AllocationQuery]]: + return (replace(previous_state, jit_mode=True), None) + + +class LabelSolarization(Operation): + """ColorJitter info added to the labels. Should be initialized in exactly the same way as + :cla:`ffcv.transforms.ColorJitter`. + """ + + def __init__( + self, solarization_prob: float = 0.5, threshold: float = 128, seed: int = None + ): + super().__init__() + self.solarization_prob = solarization_prob + self.threshold = threshold + self.seed = seed + + def generate_code(self) -> Callable: + my_range = Compiler.get_iterator() + solarization_prob = self.solarization_prob + seed = self.seed + + def solarize(labels, temp_array, indices): + temp_array[:, :-1] = labels + random.seed(seed + indices) + for i in my_range(labels.shape[0]): + if random.uniform(0, 1) < solarization_prob: + temp_array[i, -1] = 1 + else: + temp_array[i, -1] = 0 + return temp_array + + solarize.is_parallel = True + solarize.with_indices = True + + return solarize + + def declare_state_and_memory( + self, previous_state: State + ) -> Tuple[State, Optional[AllocationQuery]]: + previous_shape = previous_state.shape + new_shape = (previous_shape[0] + 1,) + return ( + replace(previous_state, shape=new_shape, dtype=np.float32), + AllocationQuery(new_shape, dtype=np.float32), + ) \ No newline at end of file From 41ec8e4bb8fc9d34797b7c91da07a6b6242f005b Mon Sep 17 00:00:00 2001 From: gent Date: Sun, 16 Jun 2024 16:59:17 +0100 Subject: [PATCH 27/31] fix error for SharedMemoryContext --- ffcv/memory_managers/shared_cache.py | 45 +++++++++++++++++----------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/ffcv/memory_managers/shared_cache.py b/ffcv/memory_managers/shared_cache.py index 31559e9c..17fb3bb0 100644 --- a/ffcv/memory_managers/shared_cache.py +++ b/ffcv/memory_managers/shared_cache.py @@ -14,16 +14,25 @@ from multiprocessing.shared_memory import SharedMemory import torch.distributed as dist +# import _posixshmem class MasterSharedMemory(SharedMemory): - def __del__(self) -> None: - if dist.is_initialized() and dist.get_rank() == 0: - print("deleting shared memory") - super().__del__() - else: - super().__del__() + def close(self): + """Closes access to the shared memory from this instance but does + not destroy the shared memory block.""" + rank = dist.get_rank() if dist.is_initialized() else 0 + print(f"[rank {rank}] deleting shared memory",force=True) + if rank != 0: return + if self._buf is not None: + self._buf.release() + self._buf = None + if self._mmap is not None: + self._mmap.close() + self._mmap = None + if self._fd >= 0: + os.close(self._fd) + self._fd = -1 class SharedMemoryContext(MemoryContext): - cache_dict = {} def __init__(self, manager:MemoryManager): self.manager = manager file_name = self.manager.reader.file_name @@ -31,21 +40,23 @@ def __init__(self, manager:MemoryManager): mmap = np.memmap(file_name, 'uint8', mode='r') size= len(mmap) + rank = dist.get_rank() if dist.is_initialized() else 0 print_args = {'force':True} if dist.is_initialized() else {} - print(f"[rank {rank}] initialize shared memory for {name}",**print_args) file = os.path.join('/dev/shm',name) - create = False if os.path.exists(file) else True - self.mem = MasterSharedMemory(name=name, create=create, size=size) - shared_mmap = np.frombuffer(self.mem.buf, dtype=np.uint8) - if rank == True: - result = filecmp.cmp(file, file_name) - if not result: - print(f"[rank {rank}] copying file to shared memory",**print_args) - shared_mmap[:] = mmap[:] - + + if rank == 0: + create = not (os.path.exists(file) and filecmp.cmp(file, file_name)) + self.mem = MasterSharedMemory(name=name, create=create, size=size) + print(f"[rank {rank}] copying file to shared memory",**print_args) + shared_mmap = np.frombuffer(self.mem.buf, dtype=np.uint8) + shared_mmap[:] = mmap[:] if dist.is_initialized(): dist.barrier() + if rank != 0: + self.mem = MasterSharedMemory(name=name, create=False, size=size) + shared_mmap = np.frombuffer(self.mem.buf, dtype=np.uint8) + print(f"[rank {rank}] opening shared memory",**print_args) self.mmap = shared_mmap @property From feb400aa0f0243b47508439b3d723c72dd091c59 Mon Sep 17 00:00:00 2001 From: "Wu, Jiantao (PG/R - Comp Sci & Elec Eng)" Date: Wed, 2 Oct 2024 18:04:28 +0100 Subject: [PATCH 28/31] benchmark --- examples/benchmark.py | 263 ++++++++++++++++++++++++++++++++++++++++++ examples/profiler.py | 4 +- 2 files changed, 264 insertions(+), 3 deletions(-) create mode 100644 examples/benchmark.py diff --git a/examples/benchmark.py b/examples/benchmark.py new file mode 100644 index 00000000..a8658945 --- /dev/null +++ b/examples/benchmark.py @@ -0,0 +1,263 @@ +import argparse +import builtins +import datetime +import json +import math +import os +import sys +import time +from pathlib import Path + +from ffcv.loader import Loader, OrderOption +import gin +import numpy as np +import timm +import torch.backends.cudnn as cudnn +from PIL import Image # a trick to solve loading lib problem +from tqdm import tqdm + +assert timm.__version__ >= "0.6.12" # version check +from torchvision import datasets +import ffcv + +from psutil import Process, net_io_counters +import socket +import json +from os import getpid + +from ffcv.transforms import ToTensor, ToDevice, ToTorchImage, NormalizeImage,RandomHorizontalFlip, View, Convert +from ffcv.fields.decoders import IntDecoder, RandomResizedCropRGBImageDecoder, SimpleRGBImageDecoder, CenterCropRGBImageDecoder + +import torch + +IMAGENET_MEAN = np.array([0.485, 0.456, 0.406]) * 255 +IMAGENET_STD = np.array([0.229, 0.224, 0.225]) * 255 + +@gin.configurable +def SimplePipeline(img_size=224,scale=(0.2,1), ratio=(3.0/4.0, 4.0/3.0),device='cuda'): + device = torch.device(device) + image_pipeline = [ + RandomResizedCropRGBImageDecoder((img_size, img_size), scale=scale,ratio=ratio,), + RandomHorizontalFlip(), + NormalizeImage(IMAGENET_MEAN, IMAGENET_STD, np.float32), + ToTensor(), ToTorchImage(), + ] + label_pipeline = [IntDecoder(), ToTensor(),ToDevice(device), View(-1)] + # Pipeline for each data field + pipelines = { + 'image': image_pipeline, + 'label': label_pipeline + } + return pipelines + + +def get_args_parser(): + parser = argparse.ArgumentParser('Data loading benchmark', add_help=False) + parser.add_argument('--batch_size', default=64, type=int, + help='Batch size per GPU (effective batch size is batch_size * accum_iter * # gpus)') + parser.add_argument('--epochs', default=5, type=int) + parser.add_argument('--img_size', default=224,type=int) + + # Dataset parameters + parser.add_argument('--data_set', default='ffcv') + parser.add_argument("--cache_type",type=int, default=0,) + parser.add_argument('--data_path', default=os.getenv("IMAGENET_DIR"), type=str, + help='dataset path') + + parser.add_argument('--output_dir', default=None, type=str, + help='path where to save, empty for no saving') + + parser.add_argument('--device', default='cuda', + help='device to use for training / testing') + parser.add_argument('--seed', default=0, type=int) + + parser.add_argument('--num_workers', default=10, type=int) + parser.add_argument('--pin_mem', action='store_true', + help='Pin CPU memory in DataLoader for more efficient (sometimes) transfer to GPU.') + parser.add_argument('--no_pin_mem', action='store_false', dest='pin_mem') + parser.set_defaults(pin_mem=True) + + # distributed training parameters + parser.add_argument('--world_size', default=1, type=int, + help='number of distributed processes') + parser.add_argument('--local-rank','--local_rank', default=-1, type=int) + parser.add_argument('--dist_on_itp', action='store_true') + parser.add_argument('--dist_url', default='env://', + help='url used to set up distributed training') + + return parser + + +class ramqdm(tqdm): + """tqdm progress bar that reports RAM usage with each update""" + _empty_desc = "using ? GB RAM; ? CPU ? IO" + _desc = "{:.2f} GB RAM; {:.2f} % CPU {:.2f} MB IO" + _GB = 10**9 + """""" + def __init__(self, *args, **kwargs): + """Override desc and get reference to current process""" + if "desc" in kwargs: + # prepend desc to the reporter mask: + self._empty_desc = kwargs["desc"] + " " + self._empty_desc + self._desc = kwargs["desc"] + " " + self._desc + del kwargs["desc"] + else: + # nothing to prepend, reporter mask is at start of sentence: + self._empty_desc = self._empty_desc.capitalize() + self._desc = self._desc.capitalize() + super().__init__(*args, desc=self._empty_desc, **kwargs) + self._process = Process(getpid()) + self.metrics = [] + """""" + def update(self, n=1): + """Calculate RAM usage and update progress bar""" + rss = self._process.memory_info().rss + ps = self._process.cpu_percent() + io_counters = self._process.io_counters().read_bytes + # net_io = net_io_counters().bytes_recv + # io_counters += net_io + + current_desc = self._desc.format(rss/self._GB, ps, io_counters/1e6) + self.set_description(current_desc) + self.metrics.append({'mem':rss/self._GB, 'cpu':ps, 'io':io_counters/1e6}) + super().update(n) + + def summary(self): + res = {} + for key in self.metrics[0].keys(): + res[key] = np.mean([i[key] for i in self.metrics]) + return res + +@gin.configurable(denylist=["args"]) +def build_dataset(args, transform_fn=SimplePipeline): + transform_train = transform_fn(img_size=args.img_size) + if args.data_set == 'IF': + # simple augmentation + dataset_train = datasets.ImageFolder(args.data_path, transform=transform_train) + elif args.data_set == 'cifar10': + dataset_train = datasets.CIFAR10(args.data_path, transform=transform_train) + elif args.data_set == 'ffcv': + order = OrderOption.RANDOM if args.distributed else OrderOption.QUASI_RANDOM + dataset_train = Loader(args.data_path, pipelines=transform_train, + batch_size=args.batch_size, num_workers=args.num_workers, + batches_ahead=4, #cache_type=args.cache_type, + order=order, distributed=args.distributed,seed=args.seed,drop_last=True) + else: + raise ValueError("Wrong dataset: ", args.data_set) + return dataset_train + +def load_one_epoch(args,loader): + start = time.time() + l=ramqdm(loader,disable=args.rank>0) + + for x1,y in l: + x1.mean() + torch.cuda.synchronize() + + end = time.time() + + if args.rank ==0: + res = l.summary() + throughput=loader.reader.num_samples/(end-start) + res['throughput'] = throughput + return res + +import torch + +def main(args): + init_distributed_mode(args) + + print('job dir: {}'.format(os.path.dirname(os.path.realpath(__file__)))) + print("{}".format(args).replace(', ', ',\n')) + + cudnn.benchmark = True + + # build dataset + dataset_train = build_dataset(args) + + num_tasks = args.world_size + global_rank = args.rank + if args.data_set != "ffcv": + sampler_train = torch.utils.data.DistributedSampler( + dataset_train, num_replicas=num_tasks, rank=global_rank, shuffle=True + ) + print("Sampler_train = %s" % str(sampler_train)) + data_loader_train = torch.utils.data.DataLoader( + dataset_train, sampler=sampler_train, + batch_size=args.batch_size, + num_workers=args.num_workers, + pin_memory=args.pin_mem, + drop_last=True, + ) + else: + data_loader_train = dataset_train + + for epoch in range(args.epochs): + res = load_one_epoch(args,data_loader_train) + if res: + throughput = res['throughput'] + print(f"Throughput: {throughput:.2f} samples/s for {args.data_path}.") + res.update(args.__dict__) + res['version'] = ffcv.__version__ + res['hostname'] = socket.gethostname() + res['epoch'] = epoch + if args.output_dir: + with open(os.path.join(args.output_dir,"data_loading.txt"),"a") as file: + file.write(json.dumps(res)+"\n") + + +def init_distributed_mode(args): + if hasattr(args,'dist_on_itp') and args.dist_on_itp: + args.rank = int(os.environ['OMPI_COMM_WORLD_RANK']) + args.world_size = int(os.environ['OMPI_COMM_WORLD_SIZE']) + args.gpu = int(os.environ['OMPI_COMM_WORLD_LOCAL_RANK']) + args.dist_url = "tcp://%s:%s" % (os.environ['MASTER_ADDR'], os.environ['MASTER_PORT']) + os.environ['LOCAL_RANK'] = str(args.gpu) + os.environ['RANK'] = str(args.rank) + os.environ['WORLD_SIZE'] = str(args.world_size) + # ["RANK", "WORLD_SIZE", "MASTER_ADDR", "MASTER_PORT", "LOCAL_RANK"] + elif 'RANK' in os.environ and 'WORLD_SIZE' in os.environ: + args.rank = int(os.environ["RANK"]) + args.world_size = int(os.environ['WORLD_SIZE']) + args.gpu = int(os.environ['LOCAL_RANK']) + elif 'SLURM_PROCID' in os.environ: + args.rank = int(os.environ['SLURM_PROCID']) + args.gpu = args.rank % torch.cuda.device_count() + else: + print('Not using distributed mode') + setup_for_distributed(is_master=True) # hack + args.distributed = False + return + + args.distributed = True + + torch.cuda.set_device(args.gpu) + args.dist_backend = 'nccl' + print('| distributed init (rank {}): {}, gpu {}'.format( + args.rank, args.dist_url, args.gpu), flush=True) + torch.distributed.init_process_group(backend=args.dist_backend, init_method=args.dist_url, + world_size=args.world_size, rank=args.rank) + torch.distributed.barrier() + setup_for_distributed(args.rank == 0) + + +def setup_for_distributed(is_master): + """ + This function disables printing when not in master process + """ + builtin_print = builtins.print + + def print(*args, **kwargs): + force = kwargs.pop('force', False) + if is_master or force: + now = datetime.datetime.now().time() + builtin_print('[{}] '.format(now), *args, **kwargs) # print with time stamp + + builtins.print = print + + + +if __name__ == '__main__': + parser = get_args_parser() + args = parser.parse_args() + main(args) diff --git a/examples/profiler.py b/examples/profiler.py index 43fade10..c5895d4d 100644 --- a/examples/profiler.py +++ b/examples/profiler.py @@ -14,8 +14,6 @@ import torch.nn as nn import torch from psutil import Process, net_io_counters - -# from torchvi import json from os import getpid @@ -73,7 +71,7 @@ def load_one_epoch(args,loader): x_std = x1.float().flatten(1).std(1) if x_std.min() <= 0: torchvision.utils.save_image(x1.float(), "invalid_sample.png", normalize=True) - assert x_std.min() > 0, "invalid sample" + # assert x_std.min() > 0, "invalid sample" end = time.time() res = l.summary() throughput=loader.reader.num_samples/(end-start) From f51bb719381cb895edabfea12eaad27c44c31f11 Mon Sep 17 00:00:00 2001 From: "Wu, Jiantao (PG/R - Comp Sci & Elec Eng)" Date: Wed, 2 Oct 2024 18:11:25 +0100 Subject: [PATCH 29/31] update comparison --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index 29748c86..368d3383 100644 --- a/README.md +++ b/README.md @@ -85,3 +85,23 @@ Compared to the original FFCV, this library has the following new features: - **lossless compression**: PNG is supported for lossless compression. We use `RGBImageField(mode='png')` to enable the lossless compression. - **few memory**: We optimize the memory usage and accelerate data loading. + +Comparison of throughput: + +| img\_size | 112 | 160 | 192 | 224 | | | | | 512 | +|--------------|--------:|--------:|--------:|:-------:|--------:|--------:|--------:|--------:|-------:| +| batch\_size | 512 | 512 | 512 | 128 | 256 | 512 | | | 512 | +| num\_workers | 10 | 10 | 10 | 10 | 10 | 5 | 10 | 20 | 10 | +| loader | | | | | | | | | | +| ours | 23024.0 | 19396.5 | 16503.6 | 16536.1 | 16338.5 | 12369.7 | 14521.4 | 14854.6 | 4260.3 | +| ffcv | 16853.2 | 13906.3 | 13598.4 | 12192.7 | 11960.2 | 9112.7 | 12539.4 | 12601.8 | 3577.8 | + +Comparison of memory usage: +| img\_size | 112 | 160 | 192 | 224 | | | | | 512 | +|--------------|-----:|-----:|-----:|:---:|-----:|-----:|-----:|-----:|-----:| +| batch\_size | 512 | 512 | 512 | 128 | 256 | 512 | | | 512 | +| num\_workers | 10 | 10 | 10 | 10 | 10 | 5 | 10 | 20 | 10 | +| loader | | | | | | | | | | +| ours | 9.0 | 9.8 | 11.4 | 5.8 | 7.7 | 11.4 | 11.4 | 11.4 | 34.0 | +| ffcv | 13.4 | 14.8 | 17.7 | 7.6 | 11.0 | 17.7 | 17.7 | 17.7 | 56.6 | + From 88052e5560b279fd80d7301b28f625ec01334f7c Mon Sep 17 00:00:00 2001 From: "Gent (PG/R - Comp Sci & Elec Eng)" Date: Tue, 28 Jan 2025 01:51:51 +0000 Subject: [PATCH 30/31] rm dependency --- .gitmodules | 3 --- examples/imagenet-example | 1 - ffcv/.DS_Store | Bin 6148 -> 0 bytes setup.py | 3 +-- 4 files changed, 1 insertion(+), 6 deletions(-) delete mode 160000 examples/imagenet-example delete mode 100644 ffcv/.DS_Store diff --git a/.gitmodules b/.gitmodules index 21f138b4..e69de29b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "examples/imagenet-example"] - path = examples/imagenet-example - url = git@github.com:libffcv/ffcv-imagenet.git diff --git a/examples/imagenet-example b/examples/imagenet-example deleted file mode 160000 index f134cbff..00000000 --- a/examples/imagenet-example +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f134cbfff7f590954edc5c24275444b7dd2f57f6 diff --git a/ffcv/.DS_Store b/ffcv/.DS_Store deleted file mode 100644 index f1fad9b32e4ff8d8848ef21e5451c2ae2fbacdc2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%}xR_5S{|cA~D&6CVFe)l|&E{4qi5hFW?$IsKMQUx^dYWc98=i*}J}wFXHn! z(-sl}9=sSMGs*O8=VzOK-E>+25S?+q0pJ3FgGyMaV6#TZPr4!nYatZ+8!0@1f)NZM zU5RGHUu1yxZU-{lvj7s@wZ;C0lk_EwRfztN@FkAYtX}^Vg<@%IyX=&ms&nT*sfnKj z**NV4qZ=AsDiw$Geh^+ogGtZbJ5xy(M9E;J6QceALvF95q^~9&HBS1e&h?GMsW_FM zdpMmoPTDouY_?`KIc+ps^_pzA>$6$K**`iyz34s1!$iFqMg{(ST6QhY;T=0`7x(Iq zl2|2==%44#a~PQcW`G&k00!*Q=TtXft-M5LfEoB119Uz}R6@^UZcrZ`*wFQn{3SvX z?9*F Date: Thu, 3 Apr 2025 02:25:10 +0000 Subject: [PATCH 31/31] remove dependency of PyTurboJPEG --- README.md | 2 +- examples/profiler.py | 57 ++++++++++++++++++++++------------------ ffcv/fields/rgb_image.py | 14 ++++++---- setup.py | 1 - 4 files changed, 42 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 368d3383..1b6a4b5b 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ This library is derived from [FFCV](https://github.com/libffcv/ffcv) to optimize ## Installation ### Running Environment ``` -conda create -y -n ffcv "python>=3.9" cupy pkg-config "libjpeg-turbo>=3.0.0" opencv numba -c conda-forge +conda create -y -n ffcv "python>=3.9" cupy pkg-config "libjpeg-turbo>=3.0.0" "opencv=4.10.0" numba -c conda-forge conda activate ffcv conda install pytorch-cuda=11.3 torchvision -c pytorch -c nvidia pip install . diff --git a/examples/profiler.py b/examples/profiler.py index c5895d4d..c959baee 100644 --- a/examples/profiler.py +++ b/examples/profiler.py @@ -68,36 +68,43 @@ def load_one_epoch(args,loader): l=ramqdm(loader) for x1,y in l: - x_std = x1.float().flatten(1).std(1) - if x_std.min() <= 0: - torchvision.utils.save_image(x1.float(), "invalid_sample.png", normalize=True) - # assert x_std.min() > 0, "invalid sample" + pass end = time.time() res = l.summary() - throughput=loader.reader.num_samples/(end-start) + try: + throughput=loader.reader.num_samples/(end-start) + except: + throughput=len(loader.dataset)/(end-start) res['throughput'] = throughput x1 = x1.float() print("Mean: ", x1.mean().item(), "Std: ", x1.std().item()) return res def main(args): - # pipe = ThreeAugmentPipeline() - pipe = { - 'image': [RandomResizedCropRGBImageDecoder((args.img_size,args.img_size)), - RandomHorizontalFlip(), - ToTensor(), - # ToDevice(torch.device('cuda')), - ToTorchImage(), - # NormalizeImage(IMAGENET_MEAN, IMAGENET_STD, np.float16), - # Convert(torch.float16), - ] - } - loader = Loader(args.data_path, batch_size=args.batch_size, num_workers=args.num_workers, - pipelines=pipe, - batches_ahead=2, distributed=False,seed=0,drop_last=True) - - decoder = loader.pipeline_specs['image'].decoder - decoder.use_crop_decode = (args.use_ffcv) + if args.no_ffcv: + tfms = torchvision.transforms.Compose([ + torchvision.transforms.RandomResizedCrop(args.img_size), + torchvision.transforms.RandomHorizontalFlip(), + torchvision.transforms.ToTensor(), + torchvision.transforms.Normalize(IMAGENET_MEAN/255, IMAGENET_STD/255), + ]) + dataset = torchvision.datasets.ImageFolder(args.data_path, transform=tfms) + loader = torch.utils.data.DataLoader(dataset, batch_size=args.batch_size, num_workers=args.num_workers, drop_last=True) + else: + pipe = { + 'image': [RandomResizedCropRGBImageDecoder((args.img_size,args.img_size)), + RandomHorizontalFlip(), + ToTensor(), + # ToDevice(torch.device('cuda')), + ToTorchImage(), + # NormalizeImage(IMAGENET_MEAN, IMAGENET_STD, np.float16), + # Convert(torch.float16), + ] + } + loader = Loader(args.data_path, batch_size=args.batch_size, num_workers=args.num_workers, + pipelines=pipe, + batches_ahead=2, distributed=False,seed=0,drop_last=True) + # warmup load_one_epoch(args,loader) @@ -109,10 +116,10 @@ def main(args): #%% if __name__ == '__main__': parser = argparse.ArgumentParser(description="FFCV Profiler") - parser.add_argument("-r", "--repeat", type=int, default=5, help="number of samples to record one step for profile.") + parser.add_argument("-r", "--repeat", type=int, default=3, help="number of samples to record one step for profile.") parser.add_argument("-b", "--batch_size", type=int, default=64, help="batch size") - parser.add_argument("-p", "--data_path", type=str, help="data path", required=True) - parser.add_argument("--use_ffcv",default=False,action="store_true") + parser.add_argument("-p", "--data_path", type=str, help="data path", required=True) + parser.add_argument("--no_ffcv",default=False,action="store_true") parser.add_argument("--num_workers", type=int, default=10, help="number of workers") parser.add_argument("--exp", default=False, action="store_true", help="run experiments") parser.add_argument("--img_size", type=int, default=224, help="image size") diff --git a/ffcv/fields/rgb_image.py b/ffcv/fields/rgb_image.py index a28a1390..b90dbde8 100644 --- a/ffcv/fields/rgb_image.py +++ b/ffcv/fields/rgb_image.py @@ -23,11 +23,15 @@ IMAGE_MODES['raw'] = 1 IMAGE_MODES['png'] = 2 -from turbojpeg import TurboJPEG, TJCS_RGB, TJSAMP_444 -turbo_jpeg = TurboJPEG() -def encode_jpeg(numpy_image, quality,jpeg_subsample=TJSAMP_444): - result = turbo_jpeg.encode(numpy_image, quality=quality, pixel_format=TJCS_RGB,jpeg_subsample=jpeg_subsample) - result = np.frombuffer(result, np.uint8) + +def encode_jpeg(numpy_image, quality): + numpy_image = cv2.cvtColor(numpy_image, cv2.COLOR_RGB2BGR) + success, result = cv2.imencode('.jpg', numpy_image, + [int(cv2.IMWRITE_JPEG_QUALITY), quality]) + + if not success: + raise ValueError("Impossible to encode image in jpeg") + return result.reshape(-1) def encode_png(numpy_image): diff --git a/setup.py b/setup.py index 9440491a..a9c9a7bb 100644 --- a/setup.py +++ b/setup.py @@ -125,5 +125,4 @@ def pkgconfig(package, kw): 'tqdm', 'psutil', 'numba', - 'PyTurboJPEG', ])