Files
2026-07-13 13:18:33 +08:00

414 lines
16 KiB
Python

# 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)