# Copyright (c) Microsoft Corporation. # SPDX-License-Identifier: Apache-2.0 # DeepSpeed Team import pytest import os import filecmp import torch import deepspeed import deepspeed.comm as dist from deepspeed.accelerator import get_accelerator from deepspeed.ops.op_builder import AsyncIOBuilder from unit.common import DistributedTest KILO_BYTE = 1024 BLOCK_SIZE = KILO_BYTE QUEUE_DEPTH = 2 IO_SIZE = 4 * BLOCK_SIZE IO_PARALLEL = 2 if not deepspeed.ops.__compatible_ops__[AsyncIOBuilder.NAME]: pytest.skip('Skip tests since async-io is not compatible', allow_module_level=True) def _skip_for_invalid_environment(use_cuda_pinned_tensor=True): if get_accelerator().device_name() != 'cuda': if use_cuda_pinned_tensor: pytest.skip("torch.pin_memory is only supported in CUDA environments.") def _get_local_rank(): if get_accelerator().is_available(): return dist.get_rank() return 0 def _get_file_path(tmpdir, file_prefix, index=0): file_suffix = f'{_get_local_rank()}_{index}' return os.path.join(tmpdir, f'{file_prefix}_{file_suffix}.pt') def _do_ref_write(tmpdir, index=0, num_bytes=IO_SIZE): ref_file = _get_file_path(tmpdir, '_py_random', index) ref_buffer = os.urandom(num_bytes) with open(ref_file, 'wb') as f: f.write(ref_buffer) return ref_file, ref_buffer def _get_test_write_file(tmpdir, index): return _get_file_path(tmpdir, '_aio_write_random', index) def _get_test_write_file_and_unpinned_tensor(tmpdir, ref_buffer, index=0): test_file = _get_test_write_file(tmpdir, index) test_buffer = get_accelerator().ByteTensor(list(ref_buffer)) return test_file, test_buffer def _get_test_write_file_and_pinned_tensor(tmpdir, ref_buffer, aio_handle=None, index=0): test_file = _get_test_write_file(tmpdir, index) if aio_handle is None: test_buffer = get_accelerator().pin_memory(torch.ByteTensor(list(ref_buffer))) else: tmp_buffer = torch.ByteTensor(list(ref_buffer)) test_buffer = aio_handle.new_cpu_locked_tensor(len(ref_buffer), tmp_buffer) test_buffer.data.copy_(tmp_buffer) return test_file, test_buffer def _validate_handle_state(handle, single_submit, overlap_events): assert handle.get_single_submit() == single_submit assert handle.get_overlap_events() == overlap_events assert handle.get_intra_op_parallelism() == IO_PARALLEL assert handle.get_block_size() == BLOCK_SIZE assert handle.get_queue_depth() == QUEUE_DEPTH @pytest.mark.parametrize("use_cuda_pinned_tensor", [True, False]) @pytest.mark.parametrize("single_submit", [True, False]) @pytest.mark.parametrize("overlap_events", [True, False]) class TestRead(DistributedTest): world_size = 1 reuse_dist_env = True requires_cuda_env = False if not get_accelerator().is_available(): init_distributed = False set_dist_env = False @pytest.mark.parametrize("use_unpinned_tensor", [True, False]) def test_parallel_read(self, tmpdir, use_cuda_pinned_tensor, single_submit, overlap_events, use_unpinned_tensor): _skip_for_invalid_environment(use_cuda_pinned_tensor=use_cuda_pinned_tensor) h = AsyncIOBuilder().load().aio_handle(BLOCK_SIZE, QUEUE_DEPTH, single_submit, overlap_events, IO_PARALLEL) if use_unpinned_tensor: aio_buffer = torch.empty(IO_SIZE, dtype=torch.uint8, device=get_accelerator().device_name()) elif use_cuda_pinned_tensor: aio_buffer = get_accelerator().pin_memory(torch.empty(IO_SIZE, dtype=torch.uint8, device='cpu')) else: aio_buffer = h.new_cpu_locked_tensor(IO_SIZE, torch.empty(0, dtype=torch.uint8)) _validate_handle_state(h, single_submit, overlap_events) ref_file, _ = _do_ref_write(tmpdir) read_status = h.sync_pread(aio_buffer, ref_file, 0) assert read_status == 1 with open(ref_file, 'rb') as f: ref_buffer = list(f.read()) assert ref_buffer == aio_buffer.tolist() if not use_cuda_pinned_tensor: h.free_cpu_locked_tensor(aio_buffer) @pytest.mark.parametrize("use_unpinned_tensor", [True, False]) def test_async_read(self, tmpdir, use_cuda_pinned_tensor, single_submit, overlap_events, use_unpinned_tensor): _skip_for_invalid_environment(use_cuda_pinned_tensor=use_cuda_pinned_tensor) use_cpu_locked_tensor = False h = AsyncIOBuilder().load().aio_handle(BLOCK_SIZE, QUEUE_DEPTH, single_submit, overlap_events, IO_PARALLEL) if use_unpinned_tensor: aio_buffer = torch.empty(IO_SIZE, dtype=torch.uint8, device=get_accelerator().device_name()) elif use_cuda_pinned_tensor: aio_buffer = get_accelerator().pin_memory(torch.empty(IO_SIZE, dtype=torch.uint8, device='cpu')) else: aio_buffer = h.new_cpu_locked_tensor(IO_SIZE, torch.empty(0, dtype=torch.uint8)) use_cpu_locked_tensor = True _validate_handle_state(h, single_submit, overlap_events) ref_file, _ = _do_ref_write(tmpdir) read_status = h.async_pread(aio_buffer, ref_file, 0) assert read_status == 0 wait_status = h.wait() assert wait_status == 1 with open(ref_file, 'rb') as f: ref_buffer = list(f.read()) assert ref_buffer == aio_buffer.tolist() if use_cpu_locked_tensor: h.free_cpu_locked_tensor(aio_buffer) @pytest.mark.parametrize("use_cuda_pinned_tensor", [True, False]) @pytest.mark.parametrize("single_submit", [True, False]) @pytest.mark.parametrize("overlap_events", [True, False]) class TestWrite(DistributedTest): world_size = 1 reuse_dist_env = True requires_cuda_env = False if not get_accelerator().is_available(): init_distributed = False set_dist_env = False @pytest.mark.parametrize("use_unpinned_tensor", [True, False]) def test_parallel_write(self, tmpdir, use_cuda_pinned_tensor, single_submit, overlap_events, use_unpinned_tensor): _skip_for_invalid_environment(use_cuda_pinned_tensor=use_cuda_pinned_tensor) ref_file, ref_buffer = _do_ref_write(tmpdir) h = AsyncIOBuilder().load().aio_handle(BLOCK_SIZE, QUEUE_DEPTH, single_submit, overlap_events, IO_PARALLEL) if use_unpinned_tensor: aio_file, aio_buffer = _get_test_write_file_and_unpinned_tensor(tmpdir, ref_buffer) if use_cuda_pinned_tensor: aio_file, aio_buffer = _get_test_write_file_and_pinned_tensor(tmpdir, ref_buffer) else: aio_file, aio_buffer = _get_test_write_file_and_pinned_tensor(tmpdir, ref_buffer, h) _validate_handle_state(h, single_submit, overlap_events) write_status = h.sync_pwrite(aio_buffer, aio_file, 0) assert write_status == 1 if not use_cuda_pinned_tensor: h.free_cpu_locked_tensor(aio_buffer) assert os.path.isfile(aio_file) filecmp.clear_cache() assert filecmp.cmp(ref_file, aio_file, shallow=False) @pytest.mark.parametrize("use_unpinned_tensor", [True, False]) def test_async_write(self, tmpdir, use_cuda_pinned_tensor, single_submit, overlap_events, use_unpinned_tensor): _skip_for_invalid_environment(use_cuda_pinned_tensor=use_cuda_pinned_tensor) ref_file, ref_buffer = _do_ref_write(tmpdir) h = AsyncIOBuilder().load().aio_handle(BLOCK_SIZE, QUEUE_DEPTH, single_submit, overlap_events, IO_PARALLEL) use_cpu_locked_tensor = False if use_unpinned_tensor: aio_file, aio_buffer = _get_test_write_file_and_unpinned_tensor(tmpdir, ref_buffer) elif use_cuda_pinned_tensor: aio_file, aio_buffer = _get_test_write_file_and_pinned_tensor(tmpdir, ref_buffer) else: aio_file, aio_buffer = _get_test_write_file_and_pinned_tensor(tmpdir, ref_buffer, h) use_cpu_locked_tensor = True _validate_handle_state(h, single_submit, overlap_events) write_status = h.async_pwrite(aio_buffer, aio_file, 0) assert write_status == 0 wait_status = h.wait() assert wait_status == 1 if use_cpu_locked_tensor: h.free_cpu_locked_tensor(aio_buffer) assert os.path.isfile(aio_file) filecmp.clear_cache() assert filecmp.cmp(ref_file, aio_file, shallow=False) @pytest.mark.sequential @pytest.mark.parametrize("use_cuda_pinned_tensor", [True, False]) @pytest.mark.parametrize("use_unpinned_tensor", [True, False]) class TestAsyncQueue(DistributedTest): world_size = 1 requires_cuda_env = False if not get_accelerator().is_available(): init_distributed = False set_dist_env = False @pytest.mark.parametrize("async_queue", [2, 3]) def test_read(self, tmpdir, async_queue, use_cuda_pinned_tensor, use_unpinned_tensor): _skip_for_invalid_environment(use_cuda_pinned_tensor=use_cuda_pinned_tensor) ref_files = [] for i in range(async_queue): f, _ = _do_ref_write(tmpdir, i) ref_files.append(f) single_submit = True overlap_events = True h = AsyncIOBuilder().load().aio_handle(BLOCK_SIZE, QUEUE_DEPTH, single_submit, overlap_events, IO_PARALLEL) use_cpu_locked_tensor = False if use_unpinned_tensor: aio_buffers = [ torch.empty(IO_SIZE, dtype=torch.uint8, device=get_accelerator().device_name()) for _ in range(async_queue) ] elif use_cuda_pinned_tensor: aio_buffers = [ get_accelerator().pin_memory(torch.empty(IO_SIZE, dtype=torch.uint8, device='cpu')) for _ in range(async_queue) ] else: tmp_tensor = torch.empty(0, dtype=torch.uint8) aio_buffers = [h.new_cpu_locked_tensor(IO_SIZE, tmp_tensor) for _ in range(async_queue)] use_cpu_locked_tensor = True _validate_handle_state(h, single_submit, overlap_events) for i in range(async_queue): read_status = h.async_pread(aio_buffers[i], ref_files[i], 0) assert read_status == 0 wait_status = h.wait() assert wait_status == async_queue for i in range(async_queue): with open(ref_files[i], 'rb') as f: ref_buffer = list(f.read()) assert ref_buffer == aio_buffers[i].tolist() if use_cpu_locked_tensor: for t in aio_buffers: h.free_cpu_locked_tensor(t) @pytest.mark.parametrize("async_queue", [2, 3]) def test_write(self, tmpdir, use_cuda_pinned_tensor, async_queue, use_unpinned_tensor): _skip_for_invalid_environment(use_cuda_pinned_tensor=use_cuda_pinned_tensor) ref_files = [] ref_buffers = [] for i in range(async_queue): f, buf = _do_ref_write(tmpdir, i) ref_files.append(f) ref_buffers.append(buf) single_submit = True overlap_events = True h = AsyncIOBuilder().load().aio_handle(BLOCK_SIZE, QUEUE_DEPTH, single_submit, overlap_events, IO_PARALLEL) aio_files = [] aio_buffers = [] for i in range(async_queue): if use_unpinned_tensor: f, buf = _get_test_write_file_and_unpinned_tensor(tmpdir, ref_buffers[i], i) elif use_cuda_pinned_tensor: f, buf = _get_test_write_file_and_pinned_tensor(tmpdir, ref_buffers[i], None, i) else: f, buf = _get_test_write_file_and_pinned_tensor(tmpdir, ref_buffers[i], h, i) aio_files.append(f) aio_buffers.append(buf) use_cpu_locked_tensor = not (use_unpinned_tensor or use_cuda_pinned_tensor) _validate_handle_state(h, single_submit, overlap_events) for i in range(async_queue): read_status = h.async_pwrite(aio_buffers[i], aio_files[i], 0) assert read_status == 0 wait_status = h.wait() assert wait_status == async_queue if use_cpu_locked_tensor: for t in aio_buffers: h.free_cpu_locked_tensor(t) for i in range(async_queue): assert os.path.isfile(aio_files[i]) filecmp.clear_cache() assert filecmp.cmp(ref_files[i], aio_files[i], shallow=False) @pytest.mark.parametrize("use_cuda_pinned_tensor", [True, False]) @pytest.mark.parametrize('file_partitions', [[1, 1, 1], [1, 1, 2], [1, 2, 1], [2, 1, 1]]) class TestAsyncFileOffset(DistributedTest): world_size = 1 @pytest.mark.parametrize('use_fd', [False, True]) def test_offset_write(self, tmpdir, file_partitions, use_cuda_pinned_tensor, use_fd): _skip_for_invalid_environment(use_cuda_pinned_tensor=use_cuda_pinned_tensor) ref_file = _get_file_path(tmpdir, '_py_random') aio_file = _get_file_path(tmpdir, '_aio_random') partition_unit_size = BLOCK_SIZE file_size = sum(file_partitions) * partition_unit_size h = AsyncIOBuilder().load().aio_handle(BLOCK_SIZE, QUEUE_DEPTH, True, True, IO_PARALLEL) if use_cuda_pinned_tensor: data_buffer = torch.ByteTensor(list(os.urandom(file_size))).pin_memory() else: data_buffer = h.new_cpu_locked_tensor(file_size, torch.empty(0, dtype=torch.uint8)) file_offsets = [] next_offset = 0 for i in range(len(file_partitions)): file_offsets.append(next_offset) next_offset += file_partitions[i] * partition_unit_size ref_fd = open(ref_file, 'wb') for i in range(len(file_partitions)): src_buffer = torch.narrow(data_buffer, 0, file_offsets[i], file_partitions[i] * partition_unit_size) ref_fd.write(src_buffer.numpy().tobytes()) ref_fd.flush() if use_fd: aio_fd = os.open(aio_file, flags=os.O_DIRECT | os.O_CREAT | os.O_WRONLY) write_status = h.async_pwrite(buffer=src_buffer, fd=aio_fd, file_offset=file_offsets[i]) else: write_status = h.async_pwrite(buffer=src_buffer, filename=aio_file, file_offset=file_offsets[i]) assert write_status == 0 wait_status = h.wait() assert wait_status == 1 if use_fd: os.path.isfile(aio_fd) os.close(aio_fd) filecmp.clear_cache() assert filecmp.cmp(ref_file, aio_file, shallow=False) ref_fd.close() if not use_cuda_pinned_tensor: h.free_cpu_locked_tensor(data_buffer) def test_offset_read(self, tmpdir, file_partitions, use_cuda_pinned_tensor): _skip_for_invalid_environment(use_cuda_pinned_tensor=use_cuda_pinned_tensor) partition_unit_size = BLOCK_SIZE file_size = sum(file_partitions) * partition_unit_size ref_file, _ = _do_ref_write(tmpdir, 0, file_size) h = AsyncIOBuilder().load().aio_handle(BLOCK_SIZE, QUEUE_DEPTH, True, True, IO_PARALLEL) if use_cuda_pinned_tensor: data_buffer = torch.zeros(file_size, dtype=torch.uint8, device='cpu').pin_memory() else: data_buffer = h.new_cpu_locked_tensor(file_size, torch.empty(0, dtype=torch.uint8)) file_offsets = [] next_offset = 0 for i in range(len(file_partitions)): file_offsets.append(next_offset) next_offset += file_partitions[i] * partition_unit_size with open(ref_file, 'rb') as ref_fd: for i in range(len(file_partitions)): ref_fd.seek(file_offsets[i]) bytes_to_read = file_partitions[i] * partition_unit_size ref_buf = list(ref_fd.read(bytes_to_read)) dst_tensor = torch.narrow(data_buffer, 0, 0, bytes_to_read) assert 1 == h.sync_pread(dst_tensor, ref_file, file_offsets[i]) assert dst_tensor.tolist() == ref_buf if not use_cuda_pinned_tensor: h.free_cpu_locked_tensor(data_buffer)