版本 | 作者 | 时间 |
---|---|---|
V1.0 | huangjiyi | 2023.7.31 |
目前 Paddle 已基本完成 PHI 算子库的独立编译 (PR#53735),在实现这个目标的过程中出现过一个问题:phi 中用到 gflags 第三方库的 Flag 定义宏在 phi 编译成动态链接库后无法在 windows 上暴露 Flag 符号,当时的做法是在 phi 下重写 Flag 定义宏 (底层仍然依赖 gflags 第三方库),使其能够在 windows 上暴露 Flag 符号 (PR#52991)
但是目前还存在 gflags 第三方库相关的另外一个问题:由于 Paddle 依然依赖了 gflags 库,外部用户同时使用 paddle C++ 库和 gflags 库时,会出现以下错误:
ERROR: something wrong with flag 'flagfile' in file '/Paddle/third_party/gflags/src/gflags.cc'. One possibility: file '/Paddle/third_party/gflags/src/gflags.cc' is being linked both statically and dynamically into this executable.
这个错误是因为在 gflags 的源文件 gflags.cc
中,会注册一些 Flag,比如 flagfile
:
DEFINE_string(flagfile, "", "load flags from file");
因为 Paddle 依赖了 gflags,所以 libpaddle.so
中也会注册 flagfile
,然后外部用户如果再依赖 gflags,会重复注册 flagfile
导致报错,gflags.cc
中的报错相关代码:
void FlagRegistry::RegisterFlag(CommandLineFlag* flag) {
Lock();
pair<FlagIterator, bool> ins =
flags_.insert(pair<const char*, CommandLineFlag*>(flag->name(), flag));
if (ins.second == false) { // means the name was already in the map
if (strcmp(ins.first->second->filename(), flag->filename()) != 0) {
ReportError(DIE, "ERROR: flag '%s' was defined more than once "
"(in files '%s' and '%s').\n",
flag->name(),
ins.first->second->filename(),
flag->filename());
} else {
ReportError(DIE, "ERROR: something wrong with flag '%s' in file '%s'. "
"One possibility: file '%s' is being linked both statically "
"and dynamically into this executable.\n",
flag->name(),
flag->filename(), flag->filename());
}
}
// Also add to the flags_by_ptr_ map.
flags_by_ptr_[flag->current_->value_buffer_] = flag;
Unlock();
}
为了解决上述问题,计划移除 Paddle 对 gflags 第三方库的依赖,在 Paddle 下实现一套独立的 flags 相关机制。
在 Paddle 下实现一套独立的 flags 相关机制,包括:
- 多种类型(bool, int32, uint32, int64, uint64, double, string)的 Flag 定义和声明宏
- 命令行参数解析,即根据命令行参数对已定义的 Flag 的 value 进行更新
- 其他 Paddle 用到的 Flag 相关操作
- 待后续补充 ...
细节要求:新实现的 flags 相关机制提供的接口与现有的接口尽可能保持一致,从而降低替换成本
待后续 Paddle 下独立的 flags 相关机制初步实现完善后,暂时将 Paddle 现有的依赖第三方库的 flags 机制保留,实现能够通过编译选项以及宏控制,选择使用哪个版本 flags 机制(需要两个版本的接口一致)
完善 Paddle 下的 flags 机制,提高框架开发者开发体验以及用户使用体验
Paddle 目前在 paddle/phi/core/flags.h
中对 gflags 中的 Flag 注册宏 DEFINE_<type>
和声明宏 DEFINE_<type>
进行了重写,重写的代码和 gflags 的实现基本一致,只是修改了一些接口名字和命名空间,同时添加了支持 Windows 下的 Flag 符号暴露,但 Paddle 目前的 Flag 注册宏和声明宏底层依然依赖的是 gflags 的代码
在 Paddle 中现有的 flags 用法主要是 Flag 注册和声明宏,以及一些 gflags 的接口:
- 目前 Paddle 中使用最多的接口是 Flag 注册和声明宏:
(PHI_)?(DEFINE|DECLARE)_<type>
,其中有PHI_
前缀的宏是 Paddle 的重写版本,底层实现与(DEFINE|DECLARE)_<type>
基本一致:(PHI_)?DEFINE_<type>(name,val, txt)
用于定义目标类型的 FLAG,会定义一个全局变量FLAGS_name
,同时进行注册,约 200+ 处用法(PHI_)?DECLARE_<type>(name)
用于声明 FLAG 全局变量,extern
用法,约 300+ 处用法
gflags::ParseCommandLineFlags(int* argc, char*** argv, bool remove_flags)
:用于解析运行时命令行输入的标志,大部分在测试文件中使用,约 20+ 处用法gflags::(GetCommandLineOption|SetCommandLineOption|AllowCommandLineReparsing|<Type>FromEnv)
:其他一些用法较少的 gflags 接口:bool GetCommandLineOption(const char* name, std::string* OUTPUT)
:用于获取 FLAG 的值,1 处用法std::string SetCommandLineOption(const char* name, const char* value)
:将value
赋值给FLAGS_name
,2 处用法void AllowCommandLineReparsing()
:允许命令行重新解析,1 处用法
ref: https://github.com/gflags/gflags
上图中只列出了一些关键数据结构及其关键成员变量和方法
-
DEFINE_<type>(name, val, txt)
:除 string 类型外,DEFINE_<type>
底层均调用DEFINE_VARIABLE
-
DECLARE_<type>(name)
:统一调用DECLARE_VARIABLE
,实现就是简单的extern
用法 -
DEFINE_VARIABLE
:关键 Flag 定义宏#define DEFINE_VARIABLE(type, shorttype, name, value, help) \ namespace fL##shorttype { \ static const type FLAGS_nono##name = value; \ /* We always want to export defined variables, dll or no */ \ GFLAGS_DLL_DEFINE_FLAG type FLAGS_##name = FLAGS_nono##name; \ static type FLAGS_no##name = FLAGS_nono##name; \ static GFLAGS_NAMESPACE::FlagRegisterer o_##name( \ #name, MAYBE_STRIPPED_HELP(help), __FILE__, \ &FLAGS_##name, &FLAGS_no##name); \ } \ using fL##shorttype::FLAGS_##name
DEFINE_VARIABLE
中定义了 3 个变量:FLAGS_##name
:全局变量,表示 Flag 当前值FLAGS_no##name
:静态全局变量,表示 Flag 默认值FLAGS_nono##name
:静态常量,只用来给FLAGS_##name
和FLAGS_no##name
赋值
这里 gflags 的解释是:当
value
是一个编译时常量时,FLAGS_nono##name
能够在编译时确定,这样能够确保FLAGS_##name
进行静态初始化(程序启动前),而不是动态初始化(程序启动后,但在main
函数之前);另外变量名称有含有no
是为了避免同时定义name
和no##name
标志,因为 gflags 支持在命令行使用--no<name>
设置FLAGS_name
为false
PS:我觉得这里有点问题:只要
value
是编译时常量,使用value
赋值同样能够确保FLAGS_##name
在静态初始化阶段进行初始化,而且只有FLAGS_##name
和FLAGS_no##name
就可以避免同时定义name
和no##name
标志了,所以完全不需要一个额外的FLAGS_nono##name
ref: https://en.cppreference.com/w/cpp/language/initialization
另外
DEFINE_string
进行了额外的实现,gflags 的解释是 std::string 不是 POD (Plain Old Data) 类型,只能进行动态初始化而不能进行静态初始化,为了尽量避免在这种情况下出现崩溃,gflags 先用 char buffer 来存放字符串,使其能够进行静态初始化,后续再使用 placement-new 构建 std::stringPS:这里有点疑惑:都是在程序启动之前进行初始化,为什么动态初始化可能会出问题,难不成可能在动态初始化之前就需要访问 Flag 吗?
感觉 gflags 关于初始化的这部分有些过度设计了,或者是因为这部分代码看记录是十几年前写的,那时候还没出 C++11.
-
FlagRegisterer
:DEFINE_VARIABLE
最后会构造一个FlagRegisterer
对象,FlagRegisterer
的构造函数的具体实现是在 Flag 注册表中注册输入的 Flag -
FlagValue
:存放标志数据指针和类型,以及一些相关操作,比较重要的是ParseFrom
,将字符串 value 转化为对应 type 的 value -
CommandLineFlag
:存放一个命令行标志的所有信息,包括 name, description, default_value 和 current_value,其中 value 用FlagValue
表示 -
FlagRegistry
:Flag 注册表,用于管理所有通过DEFINE_<type>
定义的 Flag关键成员变量:
flags
:key 为 name,value 为 flag 的查找表flags_by_ptr_
:key 为数据指针(即&FLAGS_##name
),value 为 flag 的查找表global_registry_
:注册表全局单例
关键成员函数:
void RegisterFlag(CommandLineFlag* flag)
:注册 flagCommandLineFlag* FindFlagLocked(const char* name)
:通过 name 查找 flagCommandLineFlag* FindFlagViaPtrLocked(const void* flag_ptr)
:通过数据指针查找 flagbool FlagRegistry::SetFlagLocked(CommandLineFlag* flag,const char* value)
:设置输入 flag 的 value
静态函数:
static FlagRegistry* GlobalRegistry()
:获取注册表全局单例 -
ParseCommandLineFlags(int* argc, char*** argv, ...)
:命令行标志解析函数,具体功能是对命令行运行程序时输入的标志进行解析并更新 Flag 的值,解析的逻辑主要通过CommandLineFlagParser
类实现 -
CommandLineFlagParser
:命令行标志解析实现类,关键就是实现的几个函数:ParseNewCommandLineFlags(int* argc, char*** argv, ...)
:命令行标志解析实现,具体就是从命令行输入中提取标志的 name 和 value,再调用FlagRegistry
设置 valueProcessFlagfileLocked(const string& flagval, ...)
:如果命令行中存在--flagfile <file_path>
或者再调用ParseCommandLineFlags
之前设置了FLAGS_flagfile
的值,那么就可以从提供的 flagfile 中读取一系列 flagProcessFromenvLocked(const string& flagval, ...)
:同flagfile
,如果设置了--fromenv
,--tryfromenv
或者FLAGS_fromenv
,FLAGS_tryfromenv
(value 为以,
分割的环境变量),那么就可以将环境变量的值赋给对应的 FlagProcessSingleOptionLocked(CommandLineFlag* flag, const char* value, ...)
:解析完参数后调用该函数进行设置,具体实现是调用GlobalRegistry()->SetFlagLocked(flag, value)
更新 flag,但是如果 flag name 为flagfile
,fromenv
,tryfromenv
时,会调用ProcessFlagfileLocked
或者ProcessFromenvLocked
ProcessOptionsFromStringLocked(const string& content, ...)
:ProcessFlagfileLocked
的下层实现,输入content
是文件的内容,具体实现是一行行读取并解析 Flag
ref: https://github.com/pytorch/pytorch
Pytorch 可以选择是否使用基于 gflags
库实现的 Flags
工具,具体实现方式是设置了一个编译选项以及对应的宏,默认不适用 gflags:
option(USE_GFLAGS "Use GFLAGS" OFF)
set(C10_USE_GFLAGS ${USE_GFLAGS})
实现文件:
c10/util/Flags.h
:定义 flags 接口c10/util/flags_use_gflags.cpp
:使用 gflags 第三方库实现接口(简单的封装)c10/util/flags_use_no_gflags.cpp
:不使用 gflags 的实现版本
具体实现:
-
C10_DEFINE_<type>
:用于定义特定类型的标志,统一调用C10_DEFINE_typed_var
宏 -
C10_DEFINE_typed_var
:最关键的一个宏,用于定义和注册 Flag#define C10_DEFINE_typed_var(type, name, default_value, help_str) \ C10_EXPORT type FLAGS_##name = default_value; \ namespace c10 { \ namespace { \ class C10FlagParser_##name : public C10FlagParser { \ public: \ explicit C10FlagParser_##name(const std::string& content) { \ success_ = C10FlagParser::Parse<type>(content, &FLAGS_##name); \ } \ }; \ } \ RegistererC10FlagsRegistry g_C10FlagsRegistry_##name( \ #name, \ C10FlagsRegistry(), \ RegistererC10FlagsRegistry::DefaultCreator<C10FlagParser_##name>, \ "(" #type ", default " #default_value ") " help_str); \ }
-
首先定义全局变量
FLAGS_##name
,用于存放 Flag 的值,也用于 Flag 的访问 -
然后定义了一个
C10FlagParser_##name
类,其构造函数会调用C10FlagParser::Parse
,这个函数的功能是将输入的content
字符串解析成对应 type 的值,然后赋值给FLAGS_##name
-
最后构造了一个
RegistererC10FlagsRegistry
类型的注册器对象g_C10FlagsRegistry_##name
,这个注册器对象的构造过程就是在注册表C10FlagsRegistry()
中注册一个(key, creater)
项,其中key
为#name
,creater
是RegistererC10FlagsRegistry::DefaultCreator<C10FlagParser_##name>
函数,creater
具体就是构造一个C10FlagParser_##name
对象,相当于给FLAGS_##nam
赋值 -
C10FlagsRegistry()
:用于获取 Flag 注册表单例,通过通用注册表c10::Registry
构造得到,该注册表中每一项是一个(key, creater)
对,其中key
类型为std::string
,creater
类型为返回值为std::unique_ptr<C10FlagParser>
,输入为const string&
的函数C10_EXPORT ::c10::Registry<std::string, std::unique_ptr<C10FlagParser>, const string&>* C10FlagsRegistry() { static ::c10::Registry<std::string, std::unique_ptr<C10FlagParser>, const string&>* registry = new ::c10:: Registry<std::string, std::unique_ptr<C10FlagParser>, const string&>(); return registry; }
-
RegistererC10FlagsRegistry
:Flag 注册器类型,由通用注册器类型c10::Registerer
具体化得到,其中模板参数与C10FlagsRegistry
具体化c10::Registry
的模板参数对应,该注册器的功能就是你构造一个注册器对象,就会在指定的注册表中注册一个 Flag,代码见c10/util/Registry.h
中的class Registerer
typedef ::c10::Registerer<std::string, std::unique_ptr<C10FlagParser>, const std::string&> RegistererC10FlagsRegistry;
-
综上,一个 Flag 的定义过程就是:定义 Flag 全局变量 (
FLAGS_##name
),定义 Flag 赋值函数 (C10FlagParser_##name
的构造函数),通过构造一个注册器对象在 Flag 注册表中注册key
为#name
,creater
为 Flag 赋值函数的(key, creater)
项,如果需要重新设置Flag_##name
的值可以调用 key#name
对应的creater
-
-
C10_DECLARE_<type>
:用于声明指定 Flag,统一调用C10_DECLARE_typed_var
实现,底层就是一个extern
用法:#define C10_DECLARE_typed_var(type, name) C10_API extern type FLAGS_##name
-
ParseCommandLineFlags(int* pargc, char*** pargv)
:解析命令行参数,代码主要就是解析命令行参数的一些逻辑,这部分可以看c10/util/flags_use_no_gflags.cpp
中的代码,在每个命令行参数被解析完后,会在通过C10FlagsRegistry()->Create(key, value)
给注册表中对应的 Flag 赋值。
gflags
-
优点:提供的功能很多,同时各方面都考虑的很完善
-
缺点:很多功能 Paddle 不太需要,并且一些代码实现有些过度设计的感觉,整体代码比较复杂
pytorch
-
优点:整体实现比较简洁,方便理解,同时设计比较巧妙:pytorch 没有设计 Flag 数据结构,只针对每个 Flag 设计了对应的赋值函数,然后在注册表中只存放 name, help_string, 赋值函数
-
缺点:只实现了最主要的功能,并且没有设计 Flag 数据结构,Flag 注册表也是 c10 通用注册表的一个具体化示例,不方便扩展
首先明确 Paddle 需要哪些用法及其使用场景,然后在分析这些用法如何实现
在Paddle 中现有的 gflags 用法中,Paddle 需要的用法包括:
-
DEFINE_<type>(name, val, txt)
:定义全局标志变量FLAGS_name
,并且将 flag 的一些信息进行注册 -
DECLARE_<type>(name)
:声明全局标志变量FLAGS_name
,用于需要访问FLAGS_name
的场景 -
ParseCommandLineFlags(int* argc, char*** argv, bool remove_flags)
:命令行标志解析,*argc
表示标志数量,*argv
表示标志字符串(如--name=value
)数组。-
在 Paddle 中,大部分
ParseCommandLineFlags
在测试文件中使用,用于在命令行运行测试程序时设置一些可选参数; -
还有一些地方在命令行输入 argv 的基础上,手动添加一些 flag,比如添加
--tryfromenv
设置环境变量 flag,再调用ParseCommandLineFlags
进行解析。
-
-
bool GetCommandLineOption(const char* name, std::string* OUTPUT)
:查找一个 flag,如果存在则将FLAG_##name
存放在OUTPUT
,在 Paddle 中只用到了查找功能来判断一个 flag 是否被定义 -
std::string SetCommandLineOption(const char* name, const char* value)
:用于将FLAG_##name
的值设置为value
-
void AllowCommandLineReparsing()
:Paddle 中有一处用法放在ParseCommandLineFlags
之前调用,函数名叫允许命令行重新解析,但在gflags.cc
实现代码中,这个设置只是允许ParseCommandLineFlags
传入一些未定义的 flag 而不报错
-
DEFINE_<type>
和DECLARE_<type>
如果只需要这两种用法的话,可以实现的非常简单:在
DEFINE_<type>
宏中定义一个全局变量FLAGS_##name
,在DECLARE_<type>
宏中用extern
声明这个全局变量这样的话只有当我们同时知道一个 Flag 的 name 和 type 的时候,才能用
DECLARE_<type>
访问 flag 的 value,但是在一些用法中只知道 Flag 的 name 而不知道 type (比如在命令行参数解析中),这种情况下无法使用DECLARE_<type>
访问因此需要设计一个 Flag 注册表,能够通过 name 查找到一个 flag 的 value (void* 类型数据指针) 和 type,这样在
DEFINE_<type>
宏定义完全局变量FLAGS_##name
后,还需将(name, &FLAGS_##name, type)
注册到注册表中。另外一个 Flag 的信息不仅包括 name, value, type,还有 description_string,file 等信息,因此需要设计一个 Flag 数据结构,包含一个 Flag 的完整信息,然后在注册表中可以通过 name 查找到一个完整的 Flag
-
ParseCommandLineFlags
:这部分主要是写一些标志解析逻辑,标志解析后需要调用注册表设置 value需要支持的功能:
- 普通命令行标志的解析,一般格式为
--name=value
或--name value
,需要确定支持哪些格式,主要参考 gflags - 特殊标志:
--fromenv
和--tryfromenv
,根据环境变量的值设置 Flag,Paddle 中有用到 - 考虑是否支持其他的 gflags 特殊标志,比如
--flagfile
,从一个文件中解析 Flag,Paddle 代码中没用到,不确定外部是否会用到 - 报错机制:对于不满足目标格式的 Flag 或者解析得到未定义的 Flag 的报错机制,Paddle 中用到的
AllowCommandLineReparsing()
与这个机制相关
- 普通命令行标志的解析,一般格式为
-
GetCommandLineOption
和SetCommandLineOption
:在 Flag 注册表中设计对应功能的接口即可
下面从底层数据结构开始介绍
enum class FlagType : uint8_t {
BOOL = 0,
INT32 = 1,
UINT32 = 2,
INT64 = 3,
UINT64 = 4,
DOUBLE = 5,
STRING = 6,
UNDEFINED = 7,
};
class Flag {
public:
Flag(std::string name,
std::string description,
std::string file,
FlagType type,
void* value)
: name_(name),
description_(description),
file_(file),
type_(type),
value_(value) {
}
~Flag() = default;
void SetValueFromString(const std::string& value);
private:
const std::string name_; // flag name
const std::string description_; // description message
const std::string file_; // file name where the flag is defined
const FlagType type_; // flag value type
void* value_; // flag value ptr
};
FlagType
表示 Flag 数据类型Flag
包含一个 Flag 的全部信息,主要参考了 gflags,相当于 gflags 中的CommandLineFlag
+FlagValue
,但是只保留了必要的信息和方法,这里移除了 gflags 中 value 的默认值 (Paddle没有和默认值相关的用法),只保留一个当前值SetValueFromString
:将输入的value
字符串转化为目标type_
的数值赋给value_
,在这个函数中需要检查value
是否满足目标type_
的格式
class FlagRegistry {
public:
static FlagRegistry* Instance() {
static FlagRegistry* global_registry_ = new FlagRegistry();
return global_registry_;
}
void RegisterFlag(Flag* flag);
void SetFlagValue(const std::string& name, const std::string& value);
bool HasFlag(const std::string& name);
private:
FlagRegistry() = default;
std::map<std::string, Flag*> flags_;
std::mutex mutex_;
};
FlagRegistry
为 Flag 注册表类,用于管理所有定义的 Flag- 只有一个全局单例,外部只能通过
FlagRegistry::Instance()
获取 - 主要数据:
std::map<std::string, Flag*> flags_
:name 到 Flag 指针的查找表std::mutex mutex_
:互斥锁,在修改flags_
前 lock
- 主要方法包括:
RegisterFlag
:注册 FlagSetFlagValue
:将value
string 表示的值赋给flags_[name]->value_
,HasFlag
:查找 Flag 是否存在
class FlagRegisterer {
public:
template <typename T>
FlagRegisterer(std::string name,
std::string description,
std::string file,
T* value);
};
template <typename T>
struct FlagTypeTraits {
static constexpr FlagType Type = FlagType::UNDEFINED;
};
#define DEFINE_FLAG_TYPE_TRAITS(type, flag_type) \
template <> \
struct FlagTypeTraits<type> { \
static constexpr FlagType Type = flag_type; \
}
DEFINE_FLAG_TYPE_TRAITS(bool, FlagType::BOOL);
DEFINE_FLAG_TYPE_TRAITS(int32_t, FlagType::INT32);
DEFINE_FLAG_TYPE_TRAITS(uint32_t, FlagType::UINT32);
DEFINE_FLAG_TYPE_TRAITS(int64_t, FlagType::INT64);
DEFINE_FLAG_TYPE_TRAITS(uint64_t, FlagType::UINT64);
DEFINE_FLAG_TYPE_TRAITS(double, FlagType::DOUBLE);
DEFINE_FLAG_TYPE_TRAITS(std::string, FlagType::STRING);
#undef DEFINE_FLAG_TYPE_TRAITS
template <typename T>
FlagRegisterer::FlagRegisterer(std::string name,
std::string help,
std::string file,
T* value) {
FlagType type = FlagTypeTraits<T>::Type;
Flag* flag = new Flag(name, help, file, type, value);
FlagRegistry::Instance()->RegisterFlag(flag);
}
FlagRegisterer
作为注册器,利用模板函数和结构体统一实现不同 type 的 flag 注册过程,在构造一个FlagRegisterer
对象时,会根据构造输入在 Flag 注册表中进行注册。- 其中设计了一个
FlagTypeTraits
利用模板实现内置数据类型到枚举类型FlagType
(Flag
数据结构中保存的类型) 的映射
#define PD_DEFINE_VARIABLE(type, name, value, description) \
namespace phi { \
namespace flag_##type { \
PD_EXPORT_FLAG type FLAGS_##name = value; \
/* Register FLAG */ \
static FlagRegisterer flag_##name##_registerer( \
#name, description, __FILE__, &FLAGS_##name); \
} \
} \
using phi::flag_##type::FLAGS_##name
#define PD_DEFINE_bool(name, val, txt) \
PD_DEFINE_VARIABLE(bool, name, val, txt)
#define PD_DEFINE_int32(name, val, txt) \
PD_DEFINE_VARIABLE(int32_t, name, val, txt)
#define PD_DEFINE_uint32(name, val, txt) \
PD_DEFINE_VARIABLE(uint32_t, name, val, txt)
#define PD_DEFINE_int64(name, val, txt) \
PD_DEFINE_VARIABLE(int64_t, name, val, txt)
#define PD_DEFINE_uint64(name, val, txt) \
PD_DEFINE_VARIABLE(uint64_t, name, val, txt)
#define PD_DEFINE_double(name, val, txt) \
PD_DEFINE_VARIABLE(double, name, val, txt)
#define PD_DEFINE_string(name, val, txt) \
PD_DEFINE_VARIABLE(string, name, val, txt)
PD_DEFINE_VARIABLE
:统一实现不同 type 的 Flag 定义和注册过程- 全局变量
FLAGS_##name
放在了特殊的phi::flag##type
命名空间中,然后通过 using 用法暴露出来
#define PD_DECLARE_VARIABLE(type, name) \
namespace phi { \
namespace flag_##type { \
extern PD_IMPORT_FLAG type FLAGS_##name; \
} \
} \
using phi::flag_##type::FLAGS_##name
#define PD_DECLARE_bool(name) PD_DECLARE_VARIABLE(bool, name)
#define PD_DECLARE_int32(name) PD_DECLARE_VARIABLE(int32_t, name)
#define PD_DECLARE_uint32(name) PD_DECLARE_VARIABLE(uint32_t, name)
#define PD_DECLARE_int64(name) PD_DECLARE_VARIABLE(int64_t, name)
#define PD_DECLARE_uint64(name) PD_DECLARE_VARIABLE(uint64_t, name)
#define PD_DECLARE_double(name) PD_DECLARE_VARIABLE(double, name)
#define PD_DECLARE_string(name) PD_DECLARE_VARIABLE(string, name)
PD_DECLARE_VARIABLE
:统一实现不同 type Flag 的声明,具体实现就是简单的extern
用法
实现命令行参数解析,*pargc
为参数数量,*pargv
为参数字符串数组,相邻的字符串在完整的命令中用空格分隔,其中第一个是运行的程序,大致解析逻辑如下:
void SetFlagsFromEnv(const std::vector<std::string>& envs) {
for (const std::string &env_var_name : envs) {
// 获取环境变量 env 的值, 计划实现一个函数 GetValueFromEnv
std::string value = GetValueFromEnv(env_var_name);
FlagRegistry::Instance()->SetFlagValue(env_var_name, value);
}
}
void ParseCommandLineFlags(int* pargc, char*** pargv) {
// 1. 对 pargc, pargc 进行预处理,移除第一个程序名称
size_t argv_num = *pargc - 1;
std::vector<std::string> argvs(*pargv + 1, *pargv + *pargc);
FlagRegistry* const flag_registry = FlagRegistry::Instance();
// 2. 遍历每一个 argv, 解析得到每个 flag 的 name 和 value
for (size_t i = 0; i < argv_num; i++) {
const std::string& argv = argvs[i];
// 检查 argv 格式
// ...
// 处理特殊标志 --help
if (argv == "--help" or argv == "-help") {
// 打印帮助信息
// ...
exit(1);
}
string name, value;
// 解析 name 和 value
// ...
// 处理特殊标志 --fromenv 和 --tryfromenv
if (name == "fromenv" || name == "tryfromenv") {
std::vector<std::string> envs;
// 解析需要设置的环境变量
// ...
SetFlagsFromEnv(envs);
continue;
}
flag_registry->SetFlagValue(name, value);
}
}
-
在参数格式检查中,命令行参数的格式应该满足:
--help
,--name=value
,--name value
,其中双横线--
可以换成单横线-
,value
可以放在""
中,放在""
中的value
可以包含空格,否则value
不能包含空格 -
这里说明一下不打算支持的 gflags 中的参数格式:
--name
或--noname
用于 bool flag 赋值 true 或 false- 单独的
--
表示终止解析命令行参数
-
对于特殊标志:
计划支持:
-
--help
:在
gflags
中是打印所有文件中所有的 flag 信息,包括 name, default_value, description string(我们没有定义 default_value 所以不打印了)但是Paddle 中定义了 200+ Flag,全部打印出来太多了,我认为
--help
在 Paddle 中的使用场景主要在测试中,所以不太需要打印所有 flag在 gflags 中实现了一个
--helpshort
,效果是只打印当前文件中DEFINE
的 Flag,具体通过匹配Flag
中的file_
成员实现综上,计划实现的
--help
效果是只打印当前文件中DEFINE
的 Flag,然后计划将打印所有文件中所有的 flag 信息设计成一个函数接口 -
--fromen=value
和--tryfromenv=value
:value
为用,
分隔的环境变量名env1,env2,...
,实现的效果是将环境变量name
的值赋给FLAGS_##name
,其中--tryfromenv
对于没有定义的环境变量会忽略不会宝座,--fromenv
则会报错
计划不支持的
gflags
特殊标志:- 其他过滤规则打印 Flag 信息的
--helpxxx
标志 --undefok=flagname,flagname,...
:允许列出的 Flag 没有定义而不会报错--flagfile=filepath
:从指定文件中读取 Flag,flagfile 中每一行一个 Flag
-
在代码中还需要设计一套报错机制,计划利用 Paddle 中的报错机制实现,报错主要包括以下几种情况:
- 针对
ParseCommandLineFlags
不符合目标格式参数的报错 - 针对要设置的 Flag 并没有定义(注册)的报错,这类报错可以设置一个开关函数
- 针对
SetFlagsFromEnv
中env_var_name
在环境中不存在的报错 - 针对在 Flag 注册表中注册相同 name 的 Flag 的报错
- 针对
value
字符串不满足目标 type 格式的报错
在其中几种批量处理的情况中,可以先收集每一项的错误信息再统一报错
早期实现的版本会保留目前依赖 gflags 的版本,具体参考 Pytorch 利用编译选项和宏来控制,如果新实现的版本与旧版本接口不同,会通过再封装一层来统一新旧版本的接口。
由于新实现的 Flag 注册定义宏为 PD_(DEFINE|DECLARE)_<type>
,为了实现能够切换新旧版本,旧版本的 (PHI_)?(DEFINE|DECLARE)_<type>
需要全部替换为 PD_(DEFINE|DECLARE)_<type>
,包括接口的定义和用法
- 需要将所有的
(PHI_)?(DEFINE|DECLARE)_<type>
替换为PD_(DEFINE|DECLARE)_<type>
- 其余的 gflags 用法(较少)与新实现的接口不同也需要替换
- 构建单测,验证各功能的准确性
- 测试新旧版本的一致性
- 测试新旧版本切换的编译选项
无影响
新实现的接口与目前暴露的 paddle/phi/core/flags.h
中的接口基本一致,部分接口如 ParseCommandLineFlags
因为功能相较于 gflags 更少,对于会用到新版本未实现功能的用户会有影响
无影响
无影响
- 8 月 15 日前完善设计文档,期间对于已经确定的部分进行开发
- 8 月 31 日前基本完成开发,根据 Review 意见进行修改
- 9 月 15 日前完成主要 PR 合入,后续根据反馈的问题进行修复