Program Listing for File EmitCToLaksaRef.cpp

↰ Return to documentation for file (lib/Target/EmitCToLaksaRef/EmitCToLaksaRef.cpp)

// Implementation of translation EmitCToLaksaRef
//
// @file
// @author     Jiahong Bi (jiahong.bi@tu-dresden.de)

#include "laksa-mlir/Target/EmitCToLaksaRef/LaksaRefEmitter.h"
#include "mlir/Dialect/EmitC/IR/EmitC.h"

#include "llvm/ADT/STLExtras.h"
#include "llvm/Support/CommandLine.h"

using namespace mlir;
using namespace mlir::laksa;

namespace {

llvm::cl::opt<std::string> clLaksaRefHeader(
    "laksa-ref-header",
    llvm::cl::desc(
        "Header the generated program takes the reference kernel from, as "
        "produced by the mlir-to-cpp translation"),
    llvm::cl::init("ref.h"));

llvm::cl::opt<unsigned> clLaksaRefMaxReported(
    "laksa-ref-max-reported",
    llvm::cl::desc(
        "How many differing elements the generated program spells out before "
        "going quiet"),
    llvm::cl::init(16));

// One argument of the reference kernel.
struct RefArg {
    emitc::ArrayType type;
    std::string elementType;
    std::string format; // printf conversion for one element
    std::string cast;   // what to cast an element to before printing it
    bool isRead = false;
    bool isWritten = false;
    std::string name;
    std::string boardName; // holds output<n>.bin, only when written
    std::string file;      // input<n>.bin, or output<n>.bin when write-only
    std::string fileMacro;
    std::string boardFile; // output<n>.bin when both read and written
    std::string boardFileMacro;
};

// The C spelling the mlir-to-cpp translation gives an element type.
FailureOr<std::string> getCElementType(Type type)
{
    if (auto intTy = dyn_cast<IntegerType>(type)) {
        unsigned width = intTy.getWidth();
        if (width == 1) return std::string("bool");
        if (width == 8 || width == 16 || width == 32 || width == 64)
            return (intTy.isUnsigned() ? std::string("uint")
                                       : std::string("int"))
                   + std::to_string(width) + "_t";
    }
    if (isa<Float32Type>(type)) return std::string("float");
    if (isa<Float64Type>(type)) return std::string("double");
    return failure();
}

// The only emitc.func with a body, which is the reference kernel.
FailureOr<emitc::FuncOp> findRefFunc(ModuleOp moduleOp)
{
    emitc::FuncOp found;
    for (auto funcOp : moduleOp.getOps<emitc::FuncOp>()) {
        if (funcOp.isExternal()) continue;
        if (found)
            return moduleOp.emitError(
                "expected exactly one emitc.func to check against");
        found = funcOp;
    }
    if (!found)
        return moduleOp.emitError("expected an emitc.func to check against");
    return found;
}

// Whether the kernel loads from arg, stores to it, or both.
void classifyArg(BlockArgument arg, RefArg &refArg)
{
    for (Operation* user : arg.getUsers()) {
        auto subscriptOp = dyn_cast<emitc::SubscriptOp>(user);
        if (!subscriptOp || subscriptOp->getOperand(0) != arg) continue;
        Value element = subscriptOp->getResult(0);
        for (Operation* access : element.getUsers()) {
            if (isa<emitc::LoadOp>(access)) refArg.isRead = true;
            if (auto assignOp = dyn_cast<emitc::AssignOp>(access))
                if (assignOp.getVar() == element) refArg.isWritten = true;
        }
    }
}

void emitArrayDecl(raw_ostream &os, const RefArg &arg, StringRef name)
{
    os << "static " << arg.elementType << " " << name;
    for (int64_t extent : arg.type.getShape()) os << "[" << extent << "]";
    os << ";\n";
}

void emitReadAll(raw_ostream &os)
{
    os << "static int read_all(const char* path, void* data, size_t size)\n"
          "{\n"
          "    FILE* file = fopen(path, \"rb\");\n"
          "    if (!file) {\n"
          "        fprintf(stderr, \"cannot open %s for reading\\n\", path);\n"
          "        return -1;\n"
          "    }\n"
          "    size_t got = fread(data, 1, size, file);\n"
          "    int trailing = fgetc(file) != EOF;\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"
          "    if (trailing)\n"
          "        fprintf(stderr, \"%s: ignoring bytes past %zu\\n\", path, "
          "size);\n"
          "    return 0;\n"
          "}\n";
}

void emitElapsedMs(raw_ostream &os)
{
    os << "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";
}

void emitCompare(raw_ostream &os, const RefArg &arg)
{
    ArrayRef<int64_t> shape = arg.type.getShape();
    int64_t numElements = 1;
    for (int64_t extent : shape) numElements *= extent;

    os << "static long compare_" << arg.name << "(void)\n{\n"
       << "    long mismatches = 0;\n";

    std::string indent = "    ";
    for (auto [dim, extent] : llvm::enumerate(shape)) {
        os << indent << "for (size_t i" << dim << " = 0; i" << dim << " < "
           << extent << "; ++i" << dim << ") {\n";
        indent += "    ";
    }

    std::string subscript;
    for (size_t dim = 0; dim < shape.size(); ++dim)
        subscript += "[i" + std::to_string(dim) + "]";

    os << indent << arg.cast << " want = " << arg.name << subscript << ";\n"
       << indent << arg.cast << " have = " << arg.boardName << subscript
       << ";\n"
       << indent << "if (want == have) continue;\n"
       << indent << "if (mismatches < MAX_REPORTED)\n"
       << indent << "    printf(\"" << arg.name;
    for (size_t dim = 0; dim < shape.size(); ++dim) os << "[%zu]";
    os << ": expected " << arg.format << ", got " << arg.format << "\\n\"";
    for (size_t dim = 0; dim < shape.size(); ++dim) os << ", i" << dim;
    os << ", want, have);\n"
       << indent << "else if (mismatches == MAX_REPORTED)\n"
       << indent << "    printf(\"...\\n\");\n"
       << indent << "++mismatches;\n";

    for (size_t dim = shape.size(); dim > 0; --dim) {
        indent.resize(indent.size() - 4);
        os << indent << "}\n";
    }

    os << "\n"
       << "    if (mismatches)\n"
       << "        printf(" << arg.boardFileMacro << " \": %ld of "
       << numElements << " elements differ from the reference\\n\", "
       << "mismatches);\n"
       << "    else\n"
       << "        printf(" << arg.boardFileMacro << " \": all " << numElements
       << " elements match the reference\\n\");\n"
       << "    return mismatches;\n"
          "}\n";
}

} // namespace

//===----------------------------------------------------------------------===//
// Translation entry point
//===----------------------------------------------------------------------===//

LogicalResult laksa::translateEmitCToLaksaRef(ModuleOp op, raw_ostream &os)
{
    FailureOr<emitc::FuncOp> refFunc = findRefFunc(op);
    if (failed(refFunc)) return failure();

    Block &body = refFunc->getFunctionBody().front();
    SmallVector<RefArg, 4> args(body.getNumArguments());
    unsigned numInputs = 0;
    unsigned numOutputs = 0;
    for (auto [idx, blockArg] : llvm::enumerate(body.getArguments())) {
        RefArg &arg = args[idx];
        arg.type = dyn_cast<emitc::ArrayType>(blockArg.getType());
        if (!arg.type)
            return refFunc->emitError("argument ")
                   << idx << " is not an emitc.array";

        FailureOr<std::string> elementType =
            getCElementType(arg.type.getElementType());
        if (failed(elementType))
            return refFunc->emitError("argument ")
                   << idx << " has an element type without a C spelling";
        arg.elementType = *elementType;
        bool isFloat = isa<FloatType>(arg.type.getElementType());
        arg.format = isFloat ? "%g" : "%ld";
        arg.cast = isFloat ? "double" : "long";

        classifyArg(blockArg, arg);
        if (!arg.isRead && !arg.isWritten)
            return refFunc->emitError("argument ")
                   << idx << " is neither read nor written by the reference";

        arg.name = "arg" + std::to_string(idx);
        std::string argMacro = "ARG" + std::to_string(idx);
        if (arg.isRead) {
            arg.file = "input" + std::to_string(numInputs++) + ".bin";
            arg.fileMacro = argMacro + "_IN_FILE";
        }
        if (arg.isWritten) {
            arg.boardName = arg.name + "_board";
            arg.boardFile = "output" + std::to_string(numOutputs++) + ".bin";
            arg.boardFileMacro = argMacro + "_OUT_FILE";
            if (!arg.isRead) {
                arg.file = arg.boardFile;
                arg.fileMacro = arg.boardFileMacro;
            }
        }
    }

    StringRef kernelName = refFunc->getSymName();
    bool renameKernel = kernelName == "main";
    std::string kernelCall = renameKernel ? "ref" : kernelName.str();

    os << "// Generated by ladle -t emitc-to-laksa-ref.\n"
          "// Checks what the design wrote on the board against the scalar "
          "reference in\n"
          "// \""
       << clLaksaRefHeader
       << "\", generated by\n"
          "//   ladle <input>.mlir -t mlir-to-cpp -o "
       << clLaksaRefHeader
       << "\n"
          "//\n"
          "// Build, with \""
       << clLaksaRefHeader
       << "\" next to this file:\n"
          "//   gcc -O2 -I. -o ref ref.c\n"
          "//\n"
          "// Run it where app.c left its .bin files:\n"
          "//   ./ref\n"
          "\n"
          "#include <stdbool.h>\n"
          "#include <stddef.h>\n"
          "#include <stdint.h>\n"
          "#include <stdio.h>\n"
          "#include <stdlib.h>\n"
          "#include <string.h>\n"
          "#include <time.h>\n"
          "\n";

    if (renameKernel)
        os << "// \"" << clLaksaRefHeader
           << "\" names the kernel 'main', the name it carries in the input\n"
              "// program, so rename it on the way in.\n"
              "#define main "
           << kernelCall << "\n";
    os << "#include \"" << clLaksaRefHeader << "\"\n";
    if (renameKernel) os << "#undef main\n";
    os << "\n";

    for (const RefArg &arg : args) {
        if (!arg.fileMacro.empty())
            os << "#define " << arg.fileMacro << " \"" << arg.file << "\"\n";
        if (arg.isRead && arg.isWritten)
            os << "#define " << arg.boardFileMacro << " \"" << arg.boardFile
               << "\"\n";
    }

    os << "\n"
          "// How many differing elements to spell out before going quiet.\n"
          "#define MAX_REPORTED "
       << clLaksaRefMaxReported << "\n\n";

    for (const RefArg &arg : args) {
        emitArrayDecl(os, arg, arg.name);
        if (arg.isWritten) emitArrayDecl(os, arg, arg.boardName);
    }

    os << "\n";
    emitReadAll(os);
    os << "\n";
    emitElapsedMs(os);

    for (const RefArg &arg : args) {
        if (!arg.isWritten) continue;
        os << "\n";
        emitCompare(os, arg);
    }

    os << "\nint main(void)\n{\n";
    for (const RefArg &arg : args) {
        if (arg.isRead)
            os << "    if (read_all(" << arg.fileMacro << ", " << arg.name
               << ", sizeof(" << arg.name << ")) != 0) return EXIT_FAILURE;\n";
        if (arg.isWritten)
            os << "    if (read_all(" << arg.boardFileMacro << ", "
               << arg.boardName << ", sizeof(" << arg.boardName
               << ")) != 0) return EXIT_FAILURE;\n";
    }

    os << "\n"
          "    // Only the kernel is timed, reading the files and comparing "
          "is left out.\n"
          "    struct timespec begin, end;\n"
          "    clock_gettime(CLOCK_MONOTONIC_RAW, &begin);\n"
          "    "
       << kernelCall << "(";
    llvm::interleaveComma(args, os, [&](const RefArg &arg) { os << arg.name; });
    os << ");\n"
          "    clock_gettime(CLOCK_MONOTONIC_RAW, &end);\n"
          "    printf(\"reference: ran on the CPU in %.6f ms\\n\",\n"
          "           elapsed_ms(begin, end));\n"
          "\n"
          "    long mismatches = 0;\n";
    for (const RefArg &arg : args)
        if (arg.isWritten)
            os << "    mismatches += compare_" << arg.name << "();\n";

    os << "    return mismatches ? EXIT_FAILURE : EXIT_SUCCESS;\n"
          "}\n";

    return success();
}