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();
}