LAKSA LAKSA

## About the project This repo contains **LAKSA**, the compiler infrastructure component of the [MYRTUS](https://myrtus-project.eu) project, which funds this work. All analysis, optimization, and backend code generation for LAKSA targets flow through the dialects and passes defined here. The compiler is built on top of [MLIR](https://mlir.llvm.org/), which allows each stage of the pipeline to be expressed as a dialect lowering, from high-level dataflow graph descriptions down to target-specific output. Each dialect in this project represents one layer of that pipeline. > The `DFG` dialect is a reimplementation of [Feliix42/dfg-mlir](https://github.com/Feliix42/dfg-mlir). The paper describing this project has been accepted at [CASES'26](https://esweek.org/cases/). The proceedings aren't out yet (a link to the paper will be added here later). ## Related projects [`laksa-hls-kria-driver`](https://github.com/tud-ccc/laksa-hls-kria-dirver) is a companion Linux kernel driver for deploying LAKSA-generated HLS kernels on Xilinx Kria boards (tested on the KV260, ZynqMP). It exposes `/dev/laksa`, a character device through which userspace programs manage the DMA buffer lifecycle and drive kernel execution over AXI Lite via `ioctl` calls, with no kernel recompilation needed when switching between HLS designs. ## Development Environment (Nix) This repo ships a [Nix flake](flake.nix) that provides a complete dev shell with LLVM/MLIR, `cmake`, `ninja`, `mold`, `doxygen`, `gurobi`, and the Python toolchain already on `PATH`, with `LLVM_DIR`, `MLIR_DIR`, `LLVM_EXTERNAL_LIT`, `PYTHONPATH`, `GUROBI_HOME`, and `GRB_LICENSE_FILE` set automatically: ```bash nix develop ``` If you use [direnv](https://direnv.net/), `direnv allow` will load the shell automatically via the included `.envrc`. With the shell active, [`build.sh`](build.sh) detects the toolchain env vars and configures/builds directly, no arguments needed: ```bash bash build.sh ``` ## Build on Ubuntu 24.04 (no Nix) Built and tested against [`llvm-23.1.2`](https://github.com/llvm/llvm-project/tree/llvmorg-23.1.2). Both `x86_64` and `aarch64` are supported; [`build.sh`](build.sh) reads `uname -m` and picks the matching Gurobi build (`linux64` / `armlinux64`) itself. Outside the Nix shell, [`build.sh`](build.sh) installs the system packages, fetches LLVM/MLIR and Gurobi, and sets up the Python venv, so it needs no arguments here either: ```bash bash build.sh ``` The prebuilt LLVM/MLIR tarball is about 700 MB and lands in `build/`, so the first run takes a while. ## Run in Docker Images for `linux/amd64` and `linux/arm64` are published to the GitHub Container Registry. `main` publishes `latest`, every other branch publishes under its own name: ```bash docker pull ghcr.io/tud-ccc/laksa:latest ``` The image carries the four tools on `PATH` and the `mlir_laksa` bindings importable from both the venv and the system interpreter. It starts in `/work`, so mount your working directory there: ```bash docker run --rm -it \ -v "/PATH/TO/YOUR/WORK:/work" \ -v "/PATH/TO/gurobi.lic:/opt/gurobi/gurobi.lic:ro" \ ghcr.io/tud-ccc/laksa:latest ``` The license mount is not optional for that command: the pragma DSE pass in the HLS pipeline is Gurobi-backed. Without it the entrypoint warns and only the passes that do not need Gurobi still run. Because of `--rm`, changes made inside the container are gone once it exits, unless they are in a mounted directory. To build the image from this repository instead: ```bash docker build -t laksa . ``` ## Gurobi license A license is required however you build, and nothing here creates one for you. `build.sh` only checks that the license file exists and warns if it does not; the build still succeeds without it, but anything using Gurobi fails at runtime. Put your license at `$HOME/.gurobi/gurobi.lic`, which is where both the Nix shell and `build.sh` look by default. To keep it somewhere else, point `GRB_LICENSE_FILE` at it before building: ```bash export GRB_LICENSE_FILE=/path/to/gurobi.lic ``` ## Installing ```bash cmake --install build --prefix /where/you/want ``` This gives you `bin/` with the four tools, `include/` with the headers and the TableGen output merged into one tree, `lib/` with the static libraries, and `lib/cmake/laksa/` so that a downstream CMake project can do: ```cmake find_package(LAKSA REQUIRED) target_link_libraries(my-tool PRIVATE LAKSA::DFGIR) target_include_directories(my-tool PRIVATE ${LAKSA_INCLUDE_DIRS} ${MLIR_INCLUDE_DIRS}) ``` Pass `--component` to install one piece on its own: `LAKSATools`, `LAKSAHeaders`, `LAKSALibraries`, `LAKSADevelopment`, or `LAKSAPythonModules`. ### The Python package The bindings are a `pip` package, built from **this repository**: ```bash pip install /path/to/laksa ``` While working on the bindings themselves, `cmake --build build --target install-python-package` does an editable install instead, so edits to the `.py` sources under [`python/mlir_laksa/`](python/mlir_laksa) take effect without reinstalling. `cmake --install` is not a third way to do this, and nothing it writes is a `pip` source directory. It copies the package to `/mlir_laksa`, which no interpreter looks in, as a staging copy for packagers to relocate. That path is what it is because `pip` sets the install prefix to the wheel's `platlib` directory, so the package has to sit at the root of the prefix. ## Run the tests The `.mlir` FileCheck test suite under [`test/`](test) is run with: ```bash cmake --build build --target check-laksa-mlir ``` **Using the Python bindings** See [`python/examples/`](python/examples) — it mirrors [`test/`](test)'s layout, with one `.py` file per `.mlir` test rebuilding the same IR via the Python bindings and running the same pass(es). Start there for runnable references on constructing ops and invoking passes. ```bash cmake --build build --target check-laksa-python ``` This target verifies every example still matches its `.mlir` counterpart, depending on `install-python-package` to rebuild and reinstall first. ## Build the documentation The `Doxygen` and `Sphinx` documentation is found under `build/docs/sphinx/index.html`: ```bash cmake --build build --target build-laksa-doc ``` The target installs `docs/requirements.txt` into the configured Python first. ## Compiling a design `ladle` is the driver. It dispatches to `laksa-opt` and `laksa-translate`, so everything below can also be done by hand with those two. Run one pipeline and one translation: ```bash ladle input.mlir -p convert-to-emithls -t emithls-to-cpp -o main.cpp ``` `--hls` does the whole thing instead. It lowers the input twice and writes every artifact a board deployment needs below one directory: ```bash ladle input.mlir --hls -o out_dir ``` ```text out_dir/ ├── hls.mlir the design, lowered to the EmitHLS dialect ├── ref.mlir the same input lowered to emitc instead ├── hw/ what the build host needs │ ├── main.cpp Vitis HLS input │ ├── run_hls.tcl C synthesis and IP export │ ├── run_vivado.tcl block design, synthesis, implementation, bitstream │ └── build.sh runs both, extracts .bit from the XSA └── app/ what the board needs ├── app.h buffer sizes and AXI-Lite register offsets ├── app.c userspace driver, talks to /dev/laksa ├── app.dtsi device tree overlay ├── ref.h the scalar reference ├── ref.c compares the board's output against ref.h └── run.sh loads the design, runs it, checks it ``` Every `hw/` artifact and most `app/` ones are translated from `hls.mlir`. `ref.h` and `ref.c` come from `ref.mlir`, which is the same input lowered without any of the HLS-specific restructuring, so the reference computes what the design is *meant* to compute rather than a re-derivation of what it does. Each step prints what it is producing, so a failure names the artifact that could not be written. `--num-bram` and `--num-dsp` give the pragma DSE a different resource budget than the default one of `convert-to-emithls` (by default 288 BRAMs and 1248 DSPs): ```bash ladle input.mlir --hls --num-bram=144 --num-dsp=600 -o out_dir ``` ## Running a design on the board `hw/` and `app/` are independent and can live on different machines. **On the build host**, with the Xilinx tools sourced: ```bash # In hw ./build.sh ``` This runs `vitis-run --mode hls`, then `vivado -mode batch`, then extracts `.bit` from the exported XSA. It takes a while. `` is the name of the design's top function, `main_top` for the examples under [`examples/`](examples). `.bit` is the one file the board needs out of `hw/`. Put it in the `app/` directory, next to `app.c`, and deploy that directory to the board however you like; `run.sh` looks for the bitstream beside itself and nowhere else. **On the board**, which needs [`laksa-hls-kria-driver`](https://github.com/tud-ccc/laksa-hls-kria-dirver) loaded and its `laksa.h` installed: ```bash # In app ./run.sh ``` `run.sh` compiles the overlay with `dtc`, loads it and the bitstream with `fpgautil`, builds `app` and `ref`, fills any missing `input.bin` with random bytes, runs the design, and compares what it wrote against the reference: ```text : ran on the FPGA in