Program Listing for File EmitHLSToLaksaApp.cpp
↰ Return to documentation for file (lib/Target/EmitHLSToLaksaApp/EmitHLSToLaksaApp.cpp)
// Implementation of translation EmitHLSToLaksaApp
//
// @file
// @author Jiahong Bi (jiahong.bi@tu-dresden.de)
#include "laksa-mlir/Target/EmitHLSToLaksaApp/LaksaAppEmitter.h"
#include "laksa-mlir/Target/LaksaCommon/BufferInfo.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/Support/CommandLine.h"
using namespace mlir;
using namespace mlir::emithls;
namespace {
llvm::cl::opt<std::string> clLaksaAppDevice(
"laksa-app-device",
llvm::cl::desc(
"Device node the generated application opens, as created by the "
"laksa-hls-kria-driver"),
llvm::cl::init("/dev/laksa"));
llvm::cl::opt<std::string> clLaksaAppHeader(
"laksa-app-header",
llvm::cl::desc(
"Header the generated application takes its buffer sizes and register "
"offsets from, as produced by the emithls-to-laksa-header translation"),
llvm::cl::init("app.h"));
// The identifiers and macros the generated application uses for one argument.
struct ArgNames {
std::string hostBuffer; // malloc'ed staging buffer
std::string deviceAlloc; // struct hls_alloc handle
std::string sizeMacro; // these three come from the generated header
std::string lowMacro;
std::string highMacro;
std::string inFile; // both empty unless the kernel reads the argument
std::string inFileMacro;
std::string outFile; // both empty unless the kernel writes the argument
std::string outFileMacro;
};
} // namespace
//===----------------------------------------------------------------------===//
// Translation entry point
//===----------------------------------------------------------------------===//
LogicalResult emithls::translateEmitHLSToLaksaApp(ModuleOp op, raw_ostream &os)
{
FailureOr<FuncOp> topFunc = findLaksaTopFunc(op);
if (failed(topFunc)) return failure();
SmallVector<LaksaBufferInfo> buffers;
if (failed(collectLaksaBufferInfos(*topFunc, buffers))) return failure();
StringRef designName = topFunc->getSymName();
std::string prefix = getLaksaMacroPrefix(designName);
// Inputs and outputs are numbered independently, so a design reading two
// buffers and writing one consumes input0.bin and input1.bin and produces
// output0.bin. An argument the kernel both reads and writes gets a file on
// either side.
SmallVector<ArgNames, 4> names(buffers.size());
unsigned numInputs = 0;
unsigned numOutputs = 0;
for (auto [idx, info] : llvm::enumerate(buffers)) {
std::string arg = "arg" + std::to_string(idx);
std::string argMacro = prefix + "_ARG" + std::to_string(idx);
ArgNames &name = names[idx];
name.hostBuffer = arg + "_host";
name.deviceAlloc = arg + "_buf";
name.sizeMacro = argMacro + "_SIZE";
name.lowMacro = argMacro + "_LOW_OFFSET";
name.highMacro = argMacro + "_HIGH_OFFSET";
if (info.isReadByKernel) {
name.inFile = "input" + std::to_string(numInputs++) + ".bin";
name.inFileMacro = "ARG" + std::to_string(idx) + "_IN_FILE";
}
if (info.isWrittenByKernel) {
name.outFile = "output" + std::to_string(numOutputs++) + ".bin";
name.outFileMacro = "ARG" + std::to_string(idx) + "_OUT_FILE";
}
}
os << "// Generated by ladle -t emithls-to-laksa-app.\n"
"// Userspace driver for '"
<< designName
<< "', running the design through the\n"
"// laksa-hls-kria-driver's "
<< clLaksaAppDevice
<< ". Buffer sizes and AXI-Lite register\n"
"// offsets come from \""
<< clLaksaAppHeader
<< "\", generated by\n"
"// ladle <input>.mlir -t emithls-to-laksa-header -o "
<< clLaksaAppHeader
<< "\n"
"//\n"
"// Build (after installing laksa.h to /usr/local/include), assuming "
"this file\n"
"// was written to app.c:\n"
"// gcc -O2 -I. -o app app.c\n"
"//\n"
"// Run as root, the device node requires elevated privileges:\n"
"// sudo ./app\n"
"//\n"
"// The input and output files are raw dumps of the DMA buffers "
"without any\n"
"// header, sized by the macros in \""
<< clLaksaAppHeader
<< "\".\n"
"\n"
"#include <fcntl.h>\n"
"#include <stdint.h>\n"
"#include <stdio.h>\n"
"#include <stdlib.h>\n"
"#include <sys/ioctl.h>\n"
"#include <time.h>\n"
"#include <unistd.h>\n"
"\n"
"#include <laksa.h>\n"
"\n"
"#include \""
<< clLaksaAppHeader
<< "\"\n"
"\n"
"#define DEVICE \""
<< clLaksaAppDevice << "\"\n\n";
for (const ArgNames &name : names) {
if (!name.inFile.empty())
os << "#define " << name.inFileMacro << " \"" << name.inFile
<< "\"\n";
if (!name.outFile.empty())
os << "#define " << name.outFileMacro << " \"" << name.outFile
<< "\"\n";
}
os << "\n_Static_assert(\n"
" "
<< prefix
<< "_NUM_ARGS <= HLS_MAX_ARGS,\n"
" \"the design has more m_axi arguments than the driver "
"supports\");\n"
"\n"
"/*\n"
" * A failing ioctl means the design's configuration and the "
"driver's state\n"
" * disagree, which retrying does not fix, so every one of them is "
"fatal.\n"
" */\n"
"#define CHECK_IOCTL(fd, request, arg) \\\n"
" do { \\\n"
" if (ioctl((fd), (request), (arg)) < 0) { \\\n"
" perror(#request); \\\n"
" return EXIT_FAILURE; \\\n"
" } \\\n"
" } while (0)\n"
"\n"
"static void* xmalloc(size_t size)\n"
"{\n"
" void* data = malloc(size);\n"
" if (!data) {\n"
" fprintf(stderr, \"out of memory\\n\");\n"
" exit(EXIT_FAILURE);\n"
" }\n"
" return data;\n"
"}\n"
"\n"
"static double elapsed_ms(struct timespec begin, struct timespec "
"end)\n"
"{\n"
" return (end.tv_sec - begin.tv_sec) * 1e3\n"
" + (end.tv_nsec - begin.tv_nsec) / 1e6;\n"
"}\n\n";
if (numInputs > 0)
os << "static int read_file(const char* path, void* data, size_t "
"size)\n"
"{\n"
" FILE* file = fopen(path, \"rb\");\n"
" if (!file) {\n"
" perror(path);\n"
" return -1;\n"
" }\n"
" size_t got = fread(data, 1, size, file);\n"
" fclose(file);\n"
" if (got != size) {\n"
" fprintf(stderr, \"%s: expected %zu bytes, got %zu\\n\", "
"path, size, got);\n"
" return -1;\n"
" }\n"
" return 0;\n"
"}\n\n";
if (numOutputs > 0)
os << "static int write_file(const char* path, const void* data, "
"size_t size)\n"
"{\n"
" FILE* file = fopen(path, \"wb\");\n"
" if (!file) {\n"
" perror(path);\n"
" return -1;\n"
" }\n"
" size_t put = fwrite(data, 1, size, file);\n"
" if (fclose(file) != 0 || put != size) {\n"
" fprintf(stderr, \"%s: short write, %zu of %zu "
"bytes\\n\", path, put, size);\n"
" return -1;\n"
" }\n"
" return 0;\n"
"}\n\n";
os << "int main(void)\n"
"{\n"
" int fd = open(DEVICE, O_RDWR);\n"
" if (fd < 0) {\n"
" perror(\"open \" DEVICE);\n"
" return EXIT_FAILURE;\n"
" }\n\n";
for (const ArgNames &name : names)
os << " uint8_t* " << name.hostBuffer << " = xmalloc("
<< name.sizeMacro << ");\n"
<< " struct hls_alloc " << name.deviceAlloc
<< " = {.size = " << name.sizeMacro << "};\n"
<< " CHECK_IOCTL(fd, IOCTL_ALLOC_BUFFER, &" << name.deviceAlloc
<< ");\n";
os << "\n";
for (const ArgNames &name : names) {
if (name.inFile.empty()) continue;
os << " if (read_file(" << name.inFileMacro << ", "
<< name.hostBuffer << ", " << name.sizeMacro << ") != 0)\n"
<< " return EXIT_FAILURE;\n";
os << " struct hls_transfer " << name.hostBuffer
<< "_write = {\n"
" .idx = "
<< name.deviceAlloc
<< ".idx,\n"
" .data = (uint64_t)(uintptr_t)"
<< name.hostBuffer
<< ",\n"
" .size = "
<< name.sizeMacro
<< ",\n"
" };\n"
<< " CHECK_IOCTL(fd, IOCTL_WRITE_BUFFER, &" << name.hostBuffer
<< "_write);\n\n";
}
os << " struct hls_start start = {\n"
" .ctrl_offset = "
<< prefix
<< "_CTRL_OFFSET,\n"
" .num_args = "
<< prefix
<< "_NUM_ARGS,\n"
" .args =\n"
" {\n";
for (const ArgNames &name : names)
os << " {.buf_idx = " << name.deviceAlloc << ".idx,\n"
<< " .low_offset = " << name.lowMacro << ",\n"
<< " .high_offset = " << name.highMacro << "},\n";
os << " },\n"
" };\n"
"\n"
" /*\n"
" * Only the kernel is timed, from starting it to seeing it "
"done. Moving\n"
" * the buffers to and from the device is left out.\n"
" */\n"
" struct timespec begin, end;\n"
" clock_gettime(CLOCK_MONOTONIC_RAW, &begin);\n"
" CHECK_IOCTL(fd, IOCTL_START_KERNEL, &start);\n"
"\n"
" int done = 0;\n"
" while (!done) CHECK_IOCTL(fd, IOCTL_POLL_DONE, &done);\n"
" clock_gettime(CLOCK_MONOTONIC_RAW, &end);\n"
" printf(\""
<< designName
<< ": ran on the FPGA in %.6f ms\\n\",\n"
" elapsed_ms(begin, end));\n\n";
for (const ArgNames &name : names) {
if (name.outFile.empty()) continue;
os << " struct hls_transfer " << name.hostBuffer
<< "_read = {\n"
" .idx = "
<< name.deviceAlloc
<< ".idx,\n"
" .data = (uint64_t)(uintptr_t)"
<< name.hostBuffer
<< ",\n"
" .size = "
<< name.sizeMacro
<< ",\n"
" };\n"
<< " CHECK_IOCTL(fd, IOCTL_READ_BUFFER, &" << name.hostBuffer
<< "_read);\n"
<< " if (write_file(" << name.outFileMacro << ", "
<< name.hostBuffer << ", " << name.sizeMacro << ") != 0)\n"
<< " return EXIT_FAILURE;\n"
<< " printf(\"wrote %zu bytes to %s\\n\", (size_t)"
<< name.sizeMacro << ", " << name.outFileMacro << ");\n\n";
}
for (const ArgNames &name : names)
os << " CHECK_IOCTL(fd, IOCTL_FREE_BUFFER, &" << name.deviceAlloc
<< ".idx);\n";
for (const ArgNames &name : names)
os << " free(" << name.hostBuffer << ");\n";
os << " close(fd);\n"
" return EXIT_SUCCESS;\n"
"}\n";
return success();
}