背景
TensorFlow 框架通常用在多进程和多机器环境(例如 Google 数据中心、Google Cloud Machine Learning、Amazon Web Services (AWS) 和现场分布式聚类)中。为了共享和保存 TensorFlow 生成的某些类型的状态,该框架会假定存在可靠的共享文件系统。这个共享文件系统具有诸多用途,例如:
状态检查点通常会保存到分布式文件系统中,以实现可靠性和容错能力
训练流程通过将事件文件写入受 TensorBoard 监视的目录,与 TensorBoard 通信。即使 TensorBoard 在不同的进程或机器中运行,共享文件系统也允许进行此通信
在现实世界中有许多不同的共享或分布式文件系统实现,因此 TensorFlow 使用户能够实现可向 TensorFlow 运行时注册的自定义文件系统插件。当 TensorFlow 运行时尝试通过 FileSystem 接口写入文件时,它会根据路径名的一部分动态选择应该用于文件系统操作的实现。因此,添加对自定义文件系统的支持需要实现 FileSystem 接口,编译包含该实现的共享对象,并在运行时在需要写入该文件系统的任何进程中加载该对象。
请注意,TensorFlow 已包含很多文件系统实现,例如:
标准 POSIX 文件系统
注意:NFS 文件系统通常作为 POSIX 接口装载,因此标准 TensorFlow 可以在 NFS 装载的远程文件系统上运行
HDFS - Hadoop 文件系统
GCS - Google Cloud Storage 文件系统
S3 - Amazon Simple Storage Service 文件系统
“内存映射文件” 文件系统
本指南的其余部分介绍了如何实现自定义文件系统。
实现自定义文件系统插件
要实现自定义文件系统插件,您必须执行以下操作:
实现 RandomAccessFile、WriteableFile、AppendableFile 和 ReadOnlyMemoryRegion 的子类
实现 FileSystem 接口(作为子类)
使用适当的前缀模式注册 FileSystem 实现
在需要写入该文件系统的进程中加载文件系统插件
FileSystem 接口
FileSystem 接口是在 file_system.h 中定义的抽象 C++ 接口。FileSystem 接口的实现应该实现该接口定义的所有相关方法。实现该接口需要定义一些操作,例如创建 RandomAccessFile 和 WritableFile,以及实现诸如 FileExists、IsDirectory、GetMatchingPaths、DeleteFile 等标准文件系统操作。这些接口的实现通常涉及将函数的输入参数转换为委托到已存在的库函数(实现自定义文件系统中的等效功能)。
例如,PosixFileSystem 实现会使用 POSIX unlink() 函数实现 DeleteFile;CreateDir 直接调用 mkdir();GetFileSize 涉及对文件调用 stat(),然后返回 stat 对象返回的结果报告的文件大小。同样,对于 HDFSFileSystem实现,这些调用只是委托到类似功能的 libHDFS 实现,例如为 DeleteFile 委托到 hdfsDelete。
建议您查看这些代码示例,了解不同的文件系统实现如何调用其现有库。例如:
POSIX 插件
(https://github.com/tensorflow/tensorflow/blob/master/tensorflow/core/platform/posix/posix_file_system.h?hl=zh-CN)
HDFS 插件
(https://github.com/tensorflow/tensorflow/blob/master/tensorflow/core/platform/hadoop/hadoop_file_system.h?hl=zh-CN)
GCS 插件
(https://github.com/tensorflow/tensorflow/blob/master/tensorflow/core/platform/cloud/gcs_file_system.h?hl=zh-CN)
S3 插件
(https://github.com/tensorflow/tensorflow/blob/master/tensorflow/core/platform/s3/s3_file_system.h?hl=zh-CN)
File 接口
除了允许您查询和操作文件系统中的文件和目录的操作之外,FileSystem 接口还要求您实现返回抽象对象(例如 RandomAccessFile 和 WritableFile)实现的工厂,以便 TensorFlow 代码读取和写入该 FileSystem 实现中的文件。
要实现 RandomAccessFile,您必须实现一个名为 Read() 的接口,相应实现必须在其中提供一种读取指定文件中的偏移量的方法。
例如,下面是 POSIX 文件系统的 RandomAccessFile 实现,它使用 pread() 随机访问 POSIX 函数来实现读取操作。请注意,特定实现必须知道如何从底层文件系统重试或传播错误。
class PosixRandomAccessFile : public RandomAccessFile {
public:
PosixRandomAccessFile(const string& fname, int fd)
: filename_(fname), fd_(fd) {}
~PosixRandomAccessFile() override { close(fd_); }
Status Read(uint64 offset, size_t n, StringPiece* result,
char* scratch) const override {
Status s;
char* dst = scratch;
while (n > 0 && s.ok()) {
ssize_t r = pread(fd_, dst, n, static_cast(offset));
if (r > 0) {
dst += r;
n -= r;
offset += r;
} else if (r == 0) {
s = Status(error::OUT_OF_RANGE, "Read less bytes than requested");
} else if (errno == EINTR || errno == EAGAIN) {
// Retry
} else {
s = IOError(filename_, errno);
}
}
*result = StringPiece(scratch, dst - scratch);
return s;
}
private:
string filename_;
int fd_;
};
要实现 WritableFile 顺序写入抽象功能,必须实现一些接口,例如 Append()、Flush()、Sync() 和 Close()。
例如,下面是 POSIX 文件系统的 WritableFile 实现,它会在其构造函数中接受 FILE 对象,并在该对象上使用标准 posix 函数来实现接口。
class PosixWritableFile : public WritableFile {
public:
PosixWritableFile(const string& fname, FILE* f)
: filename_(fname), file_(f) {}
~PosixWritableFile() override {
if (file_ != NULL) {
fclose(file_);
}
}
Status Append(const StringPiece& data) override {
size_t r = fwrite(data.data(), 1, data.size(), file_);
if (r != data.size()) {
return IOError(filename_, errno);
}
return Status::OK();
}
Status Close() override {
Status result;
if (fclose(file_) != 0) {
result = IOError(filename_, errno);
}
file_ = NULL;
return result;
}
Status Flush() override {
if (fflush(file_) != 0) {
return IOError(filename_, errno);
}
return Status::OK();
}
Status Sync() override {
Status s;
if (fflush(file_) != 0) {
s = IOError(filename_, errno);
}
return s;
}
private:
string filename_;
FILE* file_;
};
有关更多详情,请参阅这些接口的文档,并查看示例实现以获得启发。
注册并加载文件系统
为自定义文件系统实现了 FileSystem 实现后,您需要在 “架构” 下注册它,以便将以该架构为前缀的路径定向到您的实现。为此,您应调用 REGISTER_FILE_SYSTEM:
REGISTER_FILE_SYSTEM("foobar", FooBarFileSystem);
当 TensorFlow 尝试对其路径以 foobar:// 开头的文件执行操作时,它将使用 FooBarFileSystem 实现。
string filename = "foobar://path/to/file.txt";
std::unique_ptr file;
// Calls FooBarFileSystem::NewWritableFile to return
// a WritableFile class, which happens to be the FooBarFileSystem's
// WritableFile implementation.
TF_RETURN_IF_ERROR(env->NewWritableFile(filename, &file));
接下来,您必须编译包含此实现的共享对象。您可以在 此处 找到使用 bazel 的 cc_binary 规则执行此操作的示例,但您也可以使用任何编译系统执行此操作。有关类似说明,请参阅 编译操作库 部分(https://tensorflow.google.cn/guide/extend/adding_an_op?hl=zh-CN#build_the_op_library)。
注:此处 链接
https://github.com/tensorflow/tensorflow/blob/master/tensorflow/python/BUILD?hl=zh-CN#L244
编译此目标会生成 .so 共享对象文件。
最后,您必须在进程中动态加载此实现。在 Python 中,您可以调用 tf.load_file_system_library(file_system_library) 函数,并将路径传递给共享对象。在客户端程序中调用此函数会加载进程中的共享对象,从而将您的实现注册为可用于通过 FileSystem 接口的所有文件操作。您可以查看 test_file_system.py 了解示例(https://github.com/tensorflow/tensorflow/blob/master/tensorflow/python/framework/file_system_test.py?hl=zh-CN)。
哪些操作会使用此接口?
TensorFlow 中的几乎所有核心 C++ 文件操作(例如 CheckpointWriter、EventsWriter)以及许多其他实用程序都使用 FileSystem 接口。这意味着实现 FileSystem 实现可以让大多数 TensorFlow 程序写入共享文件系统。
在 Python 中,gfile 和 file_io 类通过 SWIG 绑定到 FileSystem 实现,这意味着在加载了此文件系统库后,您便可以运行以下命令:
with gfile.Open("foobar://path/to/file.txt") as w:
w.write("hi")
运行此命令时,包含 “hi” 的文件将出现在共享文件系统的“/path/to/file.txt”中。
更多 AI 相关阅读:
采用其他编程语言的 TensorFlow
在 TensorFlow Probability 中对结构时间序列建模