xingzhikb's picture
init
002bd9b
Raw
History Blame Contribute Delete
18.1 kB
import numpy as np
import shutil
from .common import qd_tqdm as tqdm
import mmap
import time
from .common import (
dict_update_path_value,
dict_get_path_value,
get_all_path,
load_from_yaml_str,
)
import logging
# NOTE(xiaoke): Modified. Try to use azfuse.File if possible.
try:
from azfuse import File
except ImportError:
import types
File = types.SimpleNamespace()
File.open = open
File.get_file_size = lambda x: os.stat(x).st_size
import os
import os.path as op
from contextlib import contextmanager
import subprocess
import tempfile
import hashlib
from datasets.utils.filelock import FileLock
from urllib.parse import urlparse, urlunparse
import logging
logger = logging.getLogger(__name__)
def concat_files(ins, out):
File.prepare(ins)
with File.open(out, "wb") as fp_out:
for i, f in enumerate(ins):
logging.info("concating {}/{} - {}".format(i, len(ins), f))
with File.open(f, "rb") as fp_in:
shutil.copyfileobj(fp_in, fp_out, 1024 * 1024 * 10)
def concat_tsv_files(tsvs, out_tsv):
if len(tsvs) == 1 and tsvs[0] == out_tsv:
return
File.prepare(tsvs)
concat_files(tsvs, out_tsv)
sizes = [File.get_file_size(t) for t in tsvs]
sizes = np.cumsum(sizes)
sizes = [0] + sizes[:-1].tolist()
concate_lineidx_8b(sizes, tsvs, out_tsv)
def get_tmp_folder():
folder = os.environ.get("GIT_TMP_FOLDER", "/tmp")
return folder
def parallel_map(func, all_task, num_worker=16):
if num_worker > 0:
from pathos.multiprocessing import ProcessingPool as Pool
with Pool(num_worker) as m:
result = m.map(func, all_task)
return result
else:
result = []
for t in all_task:
result.append(func(t))
return result
def ensure_remove_file(d):
if op.isfile(d) or op.islink(d):
try:
os.remove(d)
except:
pass
def concate_lineidx_8b(sizes, tsvs, out_tsv):
File.prepare(tsvs)
folder = get_tmp_folder()
def row_processor_8b(row):
offset, in_tsv, out_tsv = row
fbar = tqdm(unit_scale=True)
bulk_size = 1024
with File.open(in_tsv, "rb") as fp:
with File.open(out_tsv, "wb") as fpout:
while True:
x = fp.read(8 * bulk_size)
fbar.update(len(x) // 8)
if x != b"":
import struct
fmt = "<{}q".format(len(x) // 8)
x = [i + offset for i in struct.unpack(fmt, x)]
fpout.write(b"".join([i.to_bytes(8, "little") for i in x]))
else:
break
all_info_8b = [(sizes[i], op.splitext(t)[0] + ".lineidx.8b") for i, t in enumerate(tsvs)]
File.prepare([in_tsv for _, in_tsv in all_info_8b])
# op.join(folder, in_tsv) may also be equal to in_tsv, although it is fine
all_info_8b = [(offset, in_tsv, "{}/{}".format(folder, in_tsv + ".lineidx.8b")) for offset, in_tsv in all_info_8b]
parallel_map(row_processor_8b, all_info_8b, 64)
concat_files([i[2] for i in all_info_8b], op.splitext(out_tsv)[0] + ".lineidx.8b")
for d in all_info_8b:
ensure_remove_file(d[2])
def tsv_reader(tsv_file_name, sep="\t"):
with File.open(tsv_file_name, "r") as fp:
for i, line in enumerate(fp):
yield [x.strip() for x in line.split(sep)]
def load_from_yaml_file(file_name):
# do not use File.open as File.open depends on this function
with File.open(file_name, "r") as fp:
# with open(file_name, 'r') as fp:
data = load_from_yaml_str(fp)
while isinstance(data, dict) and "_base_" in data:
b = op.join(op.dirname(file_name), data["_base_"])
result = load_from_yaml_file(b)
assert isinstance(result, dict)
del data["_base_"]
all_key = get_all_path(data, with_list=False)
for k in all_key:
v = dict_get_path_value(data, k)
dict_update_path_value(result, k, v)
data = result
return data
def read_to_character(fp, c):
result = []
while True:
s = fp.read(32)
assert s != b"" and s != ""
if c in s:
result.append(s[: s.index(c)])
break
else:
result.append(s)
return b"".join(result)
class TSVFile(object):
def __init__(self, tsv_file, cache_policy=None, open_func=None):
self.tsv_file = tsv_file
if "://" in tsv_file:
parsed_url = urlparse(tsv_file)
path = parsed_url.path
lineidx = op.splitext(path)[0] + ".lineidx"
self.lineidx = urlunparse(parsed_url._replace(path=lineidx))
lineidx_8b = lineidx + ".8b"
self.lineidx_8b = urlunparse(parsed_url._replace(path=lineidx_8b))
else:
self.lineidx = op.splitext(tsv_file)[0] + ".lineidx"
self.lineidx_8b = self.lineidx + ".8b"
self._fp = None
self._mfp = None
self._lineidx = None
self.fp8b = None
self.cache_policy = cache_policy
self.close_fp_after_read = False
if os.environ.get("QD_TSV_CLOSE_FP_AFTER_READ"):
self.close_fp_after_read = bool(os.environ["QD_TSV_CLOSE_FP_AFTER_READ"])
self.use_mmap = False
if os.environ.get("QD_TSV_MMAP"):
self.use_mmap = int(os.environ["QD_TSV_MMAP"])
# self.has_lineidx_8b = int(os.environ.get('QD_USE_LINEIDX_8B', '0'))
self.has_lineidx_8b = True
# the process always keeps the process which opens the
# file. If the pid is not equal to the currrent pid, we will re-open
# teh file.
self.pid = None
self.lineidx_8b_pid = None
self.open_once = False
self._len = None
self._tsv_file_size = None
self.open_func = File.open if open_func is None else open_func
# NOTE: try to use azcopy
has_azcopy = subprocess.run(["azcopy"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode
self.has_azcopy = has_azcopy == 0
if self.has_azcopy:
self.temp_dir = self._get_temp_dir(tsv_file)
def _get_temp_dir(self, fname):
with tempfile.NamedTemporaryFile() as fp:
base_temp_dir = os.path.dirname(fp.name)
hash_str = hashlib.md5(fname.encode()).hexdigest()
return os.path.join(base_temp_dir, "tsv_io-" + hash_str)
@property
def tsv_file_size(self):
if self._tsv_file_size is None:
self._tsv_file_size = File.get_file_size(self.tsv_file)
return self._tsv_file_size
def get_row_len(self, i):
start = self.get_offset(i)
if i < len(self) - 1:
end = self.get_offset(i + 1)
else:
end = self.tsv_file_size
return end - start
def get_row_offsets(self, i):
start = self.get_offset(i)
if i < len(self) - 1:
end = self.get_offset(i + 1)
else:
end = self.tsv_file_size
return start, end
def _is_file_open(self, file_path):
return (
subprocess.run(
["lsof", file_path],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
).returncode
== 0
)
def _remove_unopened_file(self, file_path):
if self.temp_dir not in file_path:
return
logger.info("Try to remove file {}.".format(file_path))
if self._is_file_open(file_path):
logger.info(f"{file_path} is still open.")
else:
logger.info(f"{file_path} is all closed. So we remove it.")
if op.exists(file_path):
os.remove(file_path)
logger.info(f"Successfully remove file {file_path}.")
lock_file = file_path + ".lock"
if op.exists(lock_file):
os.remove(lock_file)
logger.info(f"Successfully remove lock file {lock_file}.")
def close_fp(self):
if self._fp:
_fp_name = self._fp.name
self._fp.close()
self._fp = None
self._remove_unopened_file(_fp_name)
if self._mfp:
_mfp_name = self._mfp.name
self._mfp.close()
self._mfp = None
self._remove_unopened_file(_mfp_name)
if self.has_lineidx_8b and self.fp8b:
fp8b_name = self.fp8b.name
self.fp8b.close()
self.fp8b = None
self._remove_unopened_file(fp8b_name)
if op.exists(self.temp_dir):
if os.listdir(self.temp_dir) == 0:
logger.info(f"{self.temp_dir} is not empty. So we do not remove it.")
else:
logger.info(f"Successfully remove temp dir {self.temp_dir} for {self.tsv_file}")
shutil.rmtree(self.temp_dir, ignore_errors=True)
def release(self):
self.close_fp()
self._lineidx = None
def close(self):
# @deprecated('use release to make it more clear not to release lineidx')
self.close_fp()
def __del__(self):
self.release()
def __str__(self):
return "TSVFile(tsv_file='{}')".format(self.tsv_file)
def __repr__(self):
return str(self)
def __iter__(self):
self._ensure_tsv_opened()
self.fp_seek(0)
if not self.use_mmap:
for line in self._fp:
result = [s.strip() for s in line.decode().split("\t")]
yield result
else:
while True:
line = self._mfp.readline()
if line == b"":
break
result = [s.strip() for s in line.decode().split("\t")]
yield result
def num_rows(self):
if self._len is None:
if self.has_lineidx_8b:
try:
self._len = File.get_file_size(self.lineidx_8b) // 8
except FileNotFoundError:
with self.open(self.lineidx_8b, "rb") as fp:
self._len = fp.seek(0, os.SEEK_END) // 8
else:
self._ensure_lineidx_loaded()
self._len = len(self._lineidx)
return self._len
def get_key(self, idx):
return self.seek_first_column(idx)
def get_current_column(self):
if self.use_mmap:
result = [s.strip() for s in self._mfp.readline().decode().split("\t")]
else:
result = [s.strip() for s in self._fp.readline().split("\t")]
return result
def get_current_column2(self, size):
if self.use_mmap:
result = [s.strip() for s in self._mfp.read(size).decode().split("\t")]
else:
result = [s.strip() for s in self._fp.read(size).decode().split("\t")]
return result
def fp_seek(self, pos):
if self.use_mmap:
self._mfp.seek(pos)
else:
self._fp.seek(pos)
def seek(self, idx):
self._ensure_tsv_opened()
pos, end = self.get_row_offsets(idx)
self.fp_seek(pos)
result = self.get_current_column2(end - pos)
if self.close_fp_after_read:
self.close_fp()
return result
def seek3(self, idx):
self._ensure_tsv_opened()
pos = self.get_offset(idx)
self.fp_seek(pos)
result = self.get_current_column()
if self.close_fp_after_read:
self.close_fp()
return result
def seek_first_column(self, idx):
self._ensure_tsv_opened()
pos = self.get_offset(idx)
self._fp.seek(pos)
return read_to_character(self._fp, b"\t").decode()
def seek_first_columns(self):
assert self.has_lineidx_8b
self._ensure_tsv_opened()
self.ensure_lineidx_8b_opened()
result = []
for idx in range(len(self)):
self.fp8b.seek(idx * 8)
pos = int.from_bytes(self.fp8b.read(8), "little")
self._fp.seek(pos)
result.append(read_to_character(self._fp, b"\t").decode())
return result
def _get_lock_file_name(self, fname):
path = urlparse(fname).path
name = op.basename(path)
return op.join(self.temp_dir, name), op.join(self.temp_dir, name + ".lock")
def open(self, fname, mode):
if "://" in fname and "blob.core.windows.net" in fname and self.has_azcopy:
if not op.isdir(self.temp_dir):
os.makedirs(self.temp_dir, exist_ok=True)
temp_file, lock_path = self._get_lock_file_name(fname)
with FileLock(lock_path):
try:
result = subprocess.run(
["azcopy", "cp", fname, temp_file],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
if result.returncode != 0:
raise ConnectionError(f"azcopy failed with return code {result.returncode}")
logger.info(f"Successfully azcopy {fname} to {temp_file}.")
return self.open_func(temp_file, mode)
except Exception as e:
logger.error(f"azcopy failed with exception {e}. Use regular xopen instead which can be slow.")
if op.isfile(temp_file):
os.remove(temp_file)
if op.isfile(lock_path):
os.remove(lock_path)
return self.open_func(fname, mode)
def ensure_lineidx_8b_opened(self):
if self.fp8b is None:
self.fp8b = self.open(self.lineidx_8b, "rb")
self.lineidx_8b_pid = os.getpid()
if self.lineidx_8b_pid != os.getpid():
self.fp8b.close()
logging.info("re-open {} because the process id changed".format(self.lineidx_8b))
self.fp8b = self.open(self.lineidx_8b, "rb")
self.lineidx_8b_pid = os.getpid()
def get_offset(self, idx):
# do not use op.isfile() to check whether lineidx_8b exists as it may
# incur API call for blobfuse, which will be super slow if we enumerate
# a bunch of data
if self.has_lineidx_8b:
self.ensure_lineidx_8b_opened()
self.fp8b.seek(idx * 8)
ret = int.from_bytes(self.fp8b.read(8), "little")
return ret
else:
self._ensure_lineidx_loaded()
pos = self._lineidx[idx]
return pos
def __getitem__(self, index):
return self.seek(index)
def __len__(self):
return self.num_rows()
def _ensure_lineidx_loaded(self):
if self._lineidx is None:
with self.open(self.lineidx, "r") as fp:
self._lineidx = tuple([int(i.strip()) for i in fp.readlines()])
logging.info("loaded {} from {}".format(len(self._lineidx), self.lineidx))
def get_tsv_fp(self):
start = time.time()
fp = self.open(self.tsv_file, "rb")
if self.use_mmap:
mfp = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ)
else:
mfp = fp
end = time.time()
if (end - start) > 10:
logging.info("too long ({}) to open {}".format(end - start, self.tsv_file))
return mfp, fp
def _ensure_tsv_opened(self):
if self.cache_policy == "memory":
assert self._fp is not None
return
if self._fp is None:
self._mfp, self._fp = self.get_tsv_fp()
self.pid = os.getpid()
if self.pid != os.getpid():
self._mfp.close()
self._fp.close()
logging.info("re-open {} because the process id changed".format(self.tsv_file))
self._mfp, self._fp = self.get_tsv_fp()
self.pid = os.getpid()
def tsv_writer(values, tsv_file_name, sep="\t"):
tsv_lineidx_file = os.path.splitext(tsv_file_name)[0] + ".lineidx"
tsv_8b_file = tsv_lineidx_file + ".8b"
idx = 0
sep = sep.encode()
with File.open(tsv_file_name, "wb") as fp, File.open(tsv_lineidx_file, "w") as fpidx, File.open(
tsv_8b_file, "wb"
) as fp8b:
assert values is not None
for value in tqdm(values):
assert value is not None
value = map(lambda v: v if type(v) == bytes else str(v).encode(), value)
v = sep.join(value) + b"\n"
fp.write(v)
fpidx.write(str(idx) + "\n")
# although we can use sys.byteorder to retrieve the system-default
# byte order, let's use little always to make it consistent and
# simple
fp8b.write(idx.to_bytes(8, "little"))
idx = idx + len(v)
# NOTE(xiaoke): Modified from tsv_writer to support context manager
@contextmanager
def TSVWriter(tsv_file_name, sep="\t"):
_tsv_writer = _TSVWriter(tsv_file_name, sep)
with File.open(_tsv_writer.tsv_file_name, "wb") as fp, File.open(
_tsv_writer.tsv_lineidx_file, "w"
) as fpidx, File.open(_tsv_writer.tsv_8b_file, "wb") as fp8b:
_tsv_writer.fp = fp
_tsv_writer.fpidx = fpidx
_tsv_writer.fp8b = fp8b
yield _tsv_writer
class _TSVWriter:
def __init__(self, tsv_file_name, sep="\t"):
self.tsv_file_name = tsv_file_name
self.tsv_lineidx_file = os.path.splitext(tsv_file_name)[0] + ".lineidx"
self.tsv_8b_file = self.tsv_lineidx_file + ".8b"
self.sep = sep.encode()
self.fp = None
self.fpidx = None
self.fp8b = None
self.idx = 0
def write(self, value):
assert value is not None
value = map(lambda v: v if type(v) == bytes else str(v).encode(), value)
v = self.sep.join(value) + b"\n"
self.fp.write(v)
self.fpidx.write(str(self.idx) + "\n")
self.fp8b.write(self.idx.to_bytes(8, "little"))
self.idx = self.idx + len(v)