From e06ba370013e6958a8c75dc8b53808c83533c905 Mon Sep 17 00:00:00 2001 From: Hanchen Ye Date: Tue, 22 Sep 2020 23:32:29 -0500 Subject: [PATCH] [QoREstimation] initial implementation; add several config files for holding profiling data, resource constraint, and tool configuration. These config file can be passed to the compiler through QoREstimation pass option --- config/op-latency.ini | 2 + config/target-spec.ini | 0 config/tool-config.ini | 2 + include/Dialect/HLSCpp/Attributes.td | 12 +- include/Dialect/HLSCpp/ParamOps.td | 36 ++- include/Dialect/HLSCpp/Passes.td | 9 + include/INIReader.h | 437 +++++++++++++++++++++++++++ lib/Dialect/HLSCpp/QoREstimation.cpp | 99 +++++- 8 files changed, 587 insertions(+), 10 deletions(-) create mode 100644 config/op-latency.ini create mode 100644 config/target-spec.ini create mode 100644 config/tool-config.ini create mode 100644 include/INIReader.h diff --git a/config/op-latency.ini b/config/op-latency.ini new file mode 100644 index 0000000..d7219e5 --- /dev/null +++ b/config/op-latency.ini @@ -0,0 +1,2 @@ +[200MHz] +op=1 diff --git a/config/target-spec.ini b/config/target-spec.ini new file mode 100644 index 0000000..e69de29 diff --git a/config/tool-config.ini b/config/tool-config.ini new file mode 100644 index 0000000..41a9cad --- /dev/null +++ b/config/tool-config.ini @@ -0,0 +1,2 @@ +[config] +frequency=200MHz diff --git a/include/Dialect/HLSCpp/Attributes.td b/include/Dialect/HLSCpp/Attributes.td index 933930a..5bb4607 100644 --- a/include/Dialect/HLSCpp/Attributes.td +++ b/include/Dialect/HLSCpp/Attributes.td @@ -6,7 +6,7 @@ #define SCALEHLS_DIALECT_HLSCPP_ATTRIBUTES_TD //===----------------------------------------------------------------------===// -// Customized Attributes +// Customized Scalar and Array Attributes //===----------------------------------------------------------------------===// def PositiveUI32Attr : Confined {} @@ -15,7 +15,7 @@ def PositiveUI32ArrayAttr : TypedArrayAttrBase {} def UI32ArrayAttr : TypedArrayAttrBase {} //===----------------------------------------------------------------------===// -// Pragma bind_storage Constraints +// Pragma bind_storage Attributes //===----------------------------------------------------------------------===// def LegalStorageType : AttrConstraint().getValue() == \"ram_t2p\""> ]>>; +def StorageTypeAttr : Confined {} + def LegalStorageImpl : AttrConstraint().getValue() == \"bram\"">, CPred<"$_self.cast().getValue() == \"lutram\"">, @@ -34,6 +36,8 @@ def LegalStorageImpl : AttrConstraint().getValue() == \"srl\""> ]>>; +def StorageImplAttr : Confined {} + //===----------------------------------------------------------------------===// // Pragma array_partition Constraints //===----------------------------------------------------------------------===// @@ -44,6 +48,8 @@ def LegalPartitionType : AttrConstraint().getValue() == \"complete\""> ]>>; +def PartitionTypeAttr : Confined {} + //===----------------------------------------------------------------------===// // Pragma bind_op Constraints //===----------------------------------------------------------------------===// @@ -57,4 +63,6 @@ def LegalOpImpl : AttrConstraint().getValue() == \"primitivedsp\""> ]>>; +def OpImplAttr : Confined {} + #endif // SCALEHLS_DIALECT_HLSCPP_ATTRIBUTES_TD diff --git a/include/Dialect/HLSCpp/ParamOps.td b/include/Dialect/HLSCpp/ParamOps.td index 1ccd202..3176b6a 100644 --- a/include/Dialect/HLSCpp/ParamOps.td +++ b/include/Dialect/HLSCpp/ParamOps.td @@ -20,8 +20,8 @@ def FuncParamOp : HLSCppOp<"func_param", [ let arguments = (ins // Pragma configuration parameters - BoolAttr : $enable_pipeline, - PositiveUI32Attr : $initial_interval, + // BoolAttr : $enable_pipeline, + // PositiveUI32Attr : $initial_interval, // Resource utilization parameters UI32Attr : $lut, @@ -50,16 +50,38 @@ def LoopParamOp : HLSCppOp<"loop_param", [ PositiveUI32Attr : $unroll_factor, // Performance parameters - UI32Attr : $bound, - UI32Attr : $nonperfect_latency, + UI32Attr : $loop_bound, + UI32Attr : $nonproc_latency, UI32Attr : $iteration_latency, - UI32Attr : $loop_latency, + UI32Attr : $latency, // Resource utilization parameters UI32Attr : $lut, UI32Attr : $dsp, UI32Attr : $bram ); + + let extraClassDeclaration = [{ + unsigned getUnrollFactor() { + return getAttrOfType("unroll_factor").getUInt(); + } + + unsigned getLoopBound() { + return getAttrOfType("loop_bound").getUInt(); + } + + unsigned getNonprocLatency() { + return getAttrOfType("nonproc_latency").getUInt(); + } + + unsigned getIterationLatency() { + return getAttrOfType("iteration_latency").getUInt(); + } + + unsigned getLatency() { + return getAttrOfType("latency").getUInt(); + } + }]; } //===----------------------------------------------------------------------===// @@ -101,8 +123,8 @@ def ArrayParamOp : HLSCppOp<"array_param", [ let arguments = (ins // Pragma configuration parameters - Confined : $storage_type, - Confined : $partition_type, + StorageTypeAttr : $storage_type, + PartitionTypeAttr : $partition_type, PositiveUI32ArrayAttr : $partition_factor, // Performance parameters diff --git a/include/Dialect/HLSCpp/Passes.td b/include/Dialect/HLSCpp/Passes.td index 808919c..a85e120 100644 --- a/include/Dialect/HLSCpp/Passes.td +++ b/include/Dialect/HLSCpp/Passes.td @@ -28,6 +28,15 @@ def QoREstimation : Pass<"hlscpp-qor-estimation", "ModuleOp"> { }]; let constructor = "mlir::scalehls::hlscpp::createQoREstimationPass()"; + + let options = [ + Option<"toolConfig", "tool-config", "std::string", + /*default=*/"\"../config/tool-config.ini\"", + "Config file path: global configurations for the ScaleHLS tools">, + Option<"opLatency", "op-latency", "std::string", + /*default=*/"\"../config/op-latency.ini\"", + "Config file path: profiling data for operation latency"> + ]; } def PragmaDSE : Pass<"hlscpp-pragma-dse", "ModuleOp"> { diff --git a/include/INIReader.h b/include/INIReader.h new file mode 100644 index 0000000..fc21325 --- /dev/null +++ b/include/INIReader.h @@ -0,0 +1,437 @@ +//===------------------------------------------------------------*- C++ -*-===// +// +// Read an INI file into easy-to-access name/value pairs. +// +// inih and INIReader are released under the New BSD license (see LICENSE.txt). +// Go to the project home page for more info: +// +// https://github.com/benhoyt/inih +// +//===----------------------------------------------------------------------===// + +#ifndef __INI_H__ +#define __INI_H__ + +/* Make this header file easier to include in C++ code */ +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/* Typedef for prototype of handler function. */ +typedef int (*ini_handler)(void *user, const char *section, const char *name, + const char *value); + +/* Typedef for prototype of fgets-style reader function. */ +typedef char *(*ini_reader)(char *str, int num, void *stream); + +/* Parse given INI-style file. May have [section]s, name=value pairs + (whitespace stripped), and comments starting with ';' (semicolon). Section + is "" if name=value pair parsed before any section heading. name:value + pairs are also supported as a concession to Python's configparser. + For each name=value pair parsed, call handler function with given user + pointer as well as section, name, and value (data only valid for duration + of handler call). Handler should return nonzero on success, zero on error. + Returns 0 on success, line number of first error on parse error (doesn't + stop on first error), -1 on file open error, or -2 on memory allocation + error (only when INI_USE_STACK is zero). +*/ +int ini_parse(const char *filename, ini_handler handler, void *user); + +/* Same as ini_parse(), but takes a FILE* instead of filename. This doesn't + close the file when it's finished -- the caller must do that. */ +int ini_parse_file(FILE *file, ini_handler handler, void *user); + +/* Same as ini_parse(), but takes an ini_reader function pointer instead of + filename. Used for implementing custom or string-based I/O. */ +int ini_parse_stream(ini_reader reader, void *stream, ini_handler handler, + void *user); + +/* Nonzero to allow multi-line value parsing, in the style of Python's + configparser. If allowed, ini_parse() will call the handler with the same + name for each subsequent line parsed. */ +#ifndef INI_ALLOW_MULTILINE +#define INI_ALLOW_MULTILINE 1 +#endif + +/* Nonzero to allow a UTF-8 BOM sequence (0xEF 0xBB 0xBF) at the start of + the file. See http://code.google.com/p/inih/issues/detail?id=21 */ +#ifndef INI_ALLOW_BOM +#define INI_ALLOW_BOM 1 +#endif + +/* Nonzero to allow inline comments (with valid inline comment characters + specified by INI_INLINE_COMMENT_PREFIXES). Set to 0 to turn off and match + Python 3.2+ configparser behaviour. */ +#ifndef INI_ALLOW_INLINE_COMMENTS +#define INI_ALLOW_INLINE_COMMENTS 1 +#endif +#ifndef INI_INLINE_COMMENT_PREFIXES +#define INI_INLINE_COMMENT_PREFIXES ";" +#endif + +/* Nonzero to use stack, zero to use heap (malloc/free). */ +#ifndef INI_USE_STACK +#define INI_USE_STACK 1 +#endif + +/* Stop parsing on first error (default is to keep parsing). */ +#ifndef INI_STOP_ON_FIRST_ERROR +#define INI_STOP_ON_FIRST_ERROR 0 +#endif + +/* Maximum line length for any line in INI file. */ +#ifndef INI_MAX_LINE +#define INI_MAX_LINE 200 +#endif + +#ifdef __cplusplus +} +#endif + +/* inih -- simple .INI file parser +inih is released under the New BSD license (see LICENSE.txt). Go to the project +home page for more info: +https://github.com/benhoyt/inih +*/ + +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) +#define _CRT_SECURE_NO_WARNINGS +#endif + +#include +#include +#include + +#if !INI_USE_STACK +#include +#endif + +#define MAX_SECTION 50 +#define MAX_NAME 50 + +/* Strip whitespace chars off end of given string, in place. Return s. */ +inline static char *rstrip(char *s) { + char *p = s + strlen(s); + while (p > s && isspace((unsigned char)(*--p))) + *p = '\0'; + return s; +} + +/* Return pointer to first non-whitespace char in given string. */ +inline static char *lskip(const char *s) { + while (*s && isspace((unsigned char)(*s))) + s++; + return (char *)s; +} + +/* Return pointer to first char (of chars) or inline comment in given string, + or pointer to null at end of string if neither found. Inline comment must + be prefixed by a whitespace character to register as a comment. */ +inline static char *find_chars_or_comment(const char *s, const char *chars) { +#if INI_ALLOW_INLINE_COMMENTS + int was_space = 0; + while (*s && (!chars || !strchr(chars, *s)) && + !(was_space && strchr(INI_INLINE_COMMENT_PREFIXES, *s))) { + was_space = isspace((unsigned char)(*s)); + s++; + } +#else + while (*s && (!chars || !strchr(chars, *s))) { + s++; + } +#endif + return (char *)s; +} + +/* Version of strncpy that ensures dest (size bytes) is null-terminated. */ +inline static char *strncpy0(char *dest, const char *src, size_t size) { + strncpy(dest, src, size); + dest[size - 1] = '\0'; + return dest; +} + +/* See documentation in header file. */ +inline int ini_parse_stream(ini_reader reader, void *stream, + ini_handler handler, void *user) { + /* Uses a fair bit of stack (use heap instead if you need to) */ +#if INI_USE_STACK + char line[INI_MAX_LINE]; +#else + char *line; +#endif + char section[MAX_SECTION] = ""; + char prev_name[MAX_NAME] = ""; + + char *start; + char *end; + char *name; + char *value; + int lineno = 0; + int error = 0; + +#if !INI_USE_STACK + line = (char *)malloc(INI_MAX_LINE); + if (!line) { + return -2; + } +#endif + + /* Scan through stream line by line */ + while (reader(line, INI_MAX_LINE, stream) != NULL) { + lineno++; + + start = line; +#if INI_ALLOW_BOM + if (lineno == 1 && (unsigned char)start[0] == 0xEF && + (unsigned char)start[1] == 0xBB && (unsigned char)start[2] == 0xBF) { + start += 3; + } +#endif + start = lskip(rstrip(start)); + + if (*start == ';' || *start == '#') { + /* Per Python configparser, allow both ; and # comments at the + start of a line */ + } +#if INI_ALLOW_MULTILINE + else if (*prev_name && *start && start > line) { + +#if INI_ALLOW_INLINE_COMMENTS + end = find_chars_or_comment(start, NULL); + if (*end) + *end = '\0'; + rstrip(start); +#endif + + /* Non-blank line with leading whitespace, treat as continuation + of previous name's value (as per Python configparser). */ + if (!handler(user, section, prev_name, start) && !error) + error = lineno; + } +#endif + else if (*start == '[') { + /* A "[section]" line */ + end = find_chars_or_comment(start + 1, "]"); + if (*end == ']') { + *end = '\0'; + strncpy0(section, start + 1, sizeof(section)); + *prev_name = '\0'; + } else if (!error) { + /* No ']' found on section line */ + error = lineno; + } + } else if (*start) { + /* Not a comment, must be a name[=:]value pair */ + end = find_chars_or_comment(start, "=:"); + if (*end == '=' || *end == ':') { + *end = '\0'; + name = rstrip(start); + value = lskip(end + 1); +#if INI_ALLOW_INLINE_COMMENTS + end = find_chars_or_comment(value, NULL); + if (*end) + *end = '\0'; +#endif + rstrip(value); + + /* Valid name[=:]value pair found, call handler */ + strncpy0(prev_name, name, sizeof(prev_name)); + if (!handler(user, section, name, value) && !error) + error = lineno; + } else if (!error) { + /* No '=' or ':' found on name[=:]value line */ + error = lineno; + } + } + +#if INI_STOP_ON_FIRST_ERROR + if (error) + break; +#endif + } + +#if !INI_USE_STACK + free(line); +#endif + + return error; +} + +/* See documentation in header file. */ +inline int ini_parse_file(FILE *file, ini_handler handler, void *user) { + return ini_parse_stream((ini_reader)fgets, file, handler, user); +} + +/* See documentation in header file. */ +inline int ini_parse(const char *filename, ini_handler handler, void *user) { + FILE *file; + int error; + + file = fopen(filename, "r"); + if (!file) + return -1; + error = ini_parse_file(file, handler, user); + fclose(file); + return error; +} + +#endif /* __INI_H__ */ + +#ifndef __INIREADER_H__ +#define __INIREADER_H__ + +#include +#include +#include + +// Read an INI file into easy-to-access name/value pairs. (Note that I've gone +// for simplicity here rather than speed, but it should be pretty decent.) +class INIReader { +public: + // Empty Constructor + INIReader(){}; + + // Construct INIReader and parse given filename. See ini.h for more info + // about the parsing. + INIReader(std::string filename); + + // Construct INIReader and parse given file. See ini.h for more info + // about the parsing. + INIReader(FILE *file); + + // Return the result of ini_parse(), i.e., 0 on success, line number of + // first error on parse error, or -1 on file open error. + int ParseError() const; + + // Return the list of sections found in ini file + const std::set &Sections() const; + + // Get a string value from INI file, returning default_value if not found. + std::string Get(std::string section, std::string name, + std::string default_value) const; + + // Get an integer (long) value from INI file, returning default_value if + // not found or not a valid integer (decimal "1234", "-1234", or hex "0x4d2"). + long GetInteger(std::string section, std::string name, + long default_value) const; + + // Get a real (floating point double) value from INI file, returning + // default_value if not found or not a valid floating point value + // according to strtod(). + double GetReal(std::string section, std::string name, + double default_value) const; + + // Get a single precision floating point number value from INI file, returning + // default_value if not found or not a valid floating point value + // according to strtof(). + float GetFloat(std::string section, std::string name, + float default_value) const; + + // Get a boolean value from INI file, returning default_value if not found or + // if not a valid true/false value. Valid true values are "true", "yes", "on", + // "1", and valid false values are "false", "no", "off", "0" (not case + // sensitive). + bool GetBoolean(std::string section, std::string name, + bool default_value) const; + +protected: + int _error; + std::map _values; + std::set _sections; + static std::string MakeKey(std::string section, std::string name); + static int ValueHandler(void *user, const char *section, const char *name, + const char *value); +}; + +#endif // __INIREADER_H__ + +#ifndef __INIREADER__ +#define __INIREADER__ + +#include +#include +#include + +inline INIReader::INIReader(std::string filename) { + _error = ini_parse(filename.c_str(), ValueHandler, this); +} + +inline INIReader::INIReader(FILE *file) { + _error = ini_parse_file(file, ValueHandler, this); +} + +inline int INIReader::ParseError() const { return _error; } + +inline const std::set &INIReader::Sections() const { + return _sections; +} + +inline std::string INIReader::Get(std::string section, std::string name, + std::string default_value) const { + std::string key = MakeKey(section, name); + return _values.count(key) ? _values.at(key) : default_value; +} + +inline long INIReader::GetInteger(std::string section, std::string name, + long default_value) const { + std::string valstr = Get(section, name, ""); + const char *value = valstr.c_str(); + char *end; + // This parses "1234" (decimal) and also "0x4D2" (hex) + long n = strtol(value, &end, 0); + return end > value ? n : default_value; +} + +inline double INIReader::GetReal(std::string section, std::string name, + double default_value) const { + std::string valstr = Get(section, name, ""); + const char *value = valstr.c_str(); + char *end; + double n = strtod(value, &end); + return end > value ? n : default_value; +} + +inline float INIReader::GetFloat(std::string section, std::string name, + float default_value) const { + std::string valstr = Get(section, name, ""); + const char *value = valstr.c_str(); + char *end; + float n = strtof(value, &end); + return end > value ? n : default_value; +} + +inline bool INIReader::GetBoolean(std::string section, std::string name, + bool default_value) const { + std::string valstr = Get(section, name, ""); + // Convert to lower case to make string comparisons case-insensitive + std::transform(valstr.begin(), valstr.end(), valstr.begin(), ::tolower); + if (valstr == "true" || valstr == "yes" || valstr == "on" || valstr == "1") + return true; + else if (valstr == "false" || valstr == "no" || valstr == "off" || + valstr == "0") + return false; + else + return default_value; +} + +inline std::string INIReader::MakeKey(std::string section, std::string name) { + std::string key = section + "=" + name; + // Convert to lower case to make section/name lookups case-insensitive + std::transform(key.begin(), key.end(), key.begin(), ::tolower); + return key; +} + +inline int INIReader::ValueHandler(void *user, const char *section, + const char *name, const char *value) { + INIReader *reader = (INIReader *)user; + std::string key = MakeKey(section, name); + if (reader->_values[key].size() > 0) + reader->_values[key] += "\n"; + reader->_values[key] += value; + reader->_sections.insert(section); + return 1; +} + +#endif // __INIREADER__ diff --git a/lib/Dialect/HLSCpp/QoREstimation.cpp b/lib/Dialect/HLSCpp/QoREstimation.cpp index db78c61..317b2bb 100644 --- a/lib/Dialect/HLSCpp/QoREstimation.cpp +++ b/lib/Dialect/HLSCpp/QoREstimation.cpp @@ -4,14 +4,111 @@ #include "Dialect/HLSCpp/HLSCpp.h" #include "Dialect/HLSCpp/Passes.h" +#include "INIReader.h" using namespace mlir; using namespace scalehls; using namespace hlscpp; +namespace { +class QoREstimator { +public: + explicit QoREstimator(std::string toolConfigPath, std::string opLatencyPath) { + INIReader toolConfig(toolConfigPath); + if (toolConfig.ParseError()) + llvm::outs() << "error: Tool configuration file parse fail.\n"; + + INIReader opLatency(opLatencyPath); + if (opLatency.ParseError()) + llvm::outs() << "error: Op latency file parse fail.\n"; + + auto freq = toolConfig.Get("config", "frequency", "200MHz"); + auto latency = opLatency.GetInteger(freq, "op", 0); + llvm::outs() << latency << "\n"; + } + + void estimateLoop(AffineForOp loop); + void estimateFunc(FuncOp func); + void estimateModule(ModuleOp module); + +private: +}; +} // namespace + +/// For now, estimation for unrolled loops are following the analytical model +/// of COMBA, which is suspected to be wrong. Meanwhile, we assume the absence +/// of function call in the loop body. +void QoREstimator::estimateLoop(AffineForOp loop) { + auto &body = loop.getLoopBody(); + if (body.getBlocks().size() != 1) + loop.emitError("has zero or more than one basic blocks."); + + auto paramOp = dyn_cast(body.front().front()); + if (!paramOp) { + loop.emitError("doesn't have parameter operations as front."); + return; + } + + // TODO: a simple AEAP scheduling. + unsigned iterLatency = paramOp.getNonprocLatency(); + for (auto &op : body.front()) { + if (auto subLoop = dyn_cast(op)) { + estimateLoop(subLoop); + auto subParamOp = + dyn_cast(subLoop.getLoopBody().front().front()); + iterLatency += subParamOp.getLatency(); + } + } + + unsigned latency = iterLatency; + // When loop is not completely unrolled. + if (paramOp.getLoopBound() > 1) + latency = iterLatency * paramOp.getLoopBound() * paramOp.getUnrollFactor(); + auto builder = Builder(paramOp.getContext()); + paramOp.setAttr("latency", builder.getUI32IntegerAttr(latency)); +} + +/// For now, function pipelining and task-level dataflow optimizations are not +/// considered for simplicity. +void QoREstimator::estimateFunc(FuncOp func) { + if (func.getBlocks().size() != 1) + func.emitError("has zero or more than one basic blocks."); + + auto paramOp = dyn_cast(func.front().front()); + if (!paramOp) { + func.emitError("doesn't have parameter operations as front."); + return; + } + + // Recursively estimate latency of sub-elements, including functions and + // loops. These sub-elements will be considered as a normal node in the CDFG + // for function latency estimzation. + for (auto &op : func.front()) { + if (auto subFunc = dyn_cast(op)) + estimateFunc(subFunc); + else if (auto subLoop = dyn_cast(op)) + estimateLoop(subLoop); + } + + // Estimate function latency. + for (auto &op : func.front()) { + } +} + +void QoREstimator::estimateModule(ModuleOp module) { + for (auto &op : module) { + if (auto func = dyn_cast(op)) + estimateFunc(func); + else if (!isa(op)) + op.emitError("is unsupported operation."); + } +} + namespace { struct QoREstimation : public QoREstimationBase { - void runOnOperation() {} + void runOnOperation() override { + QoREstimator(toolConfig, opLatency).estimateModule(getOperation()); + } }; } // namespace