Splyce is an MLIR optimization pass that vectorizes 2-way co-iteration loops in sparse tensor computations. It replaces scalar scf.while epilogues with SIMD fast-lane execution paths, delivering significant speedups for sparse tensor kernels.
- Overview
- Prerequisites
- Building and Installation
- Getting Started
- Docker
- Evaluation
- Troubleshooting
- License
Splyce tackles the vectorization bottleneck in sparse tensor computations. Traditional sparse dialect lowering produces scalar coiteration loops that:
- Check tensor coordinates on each iteration
- Branch based on coordinate comparisons
- Cannot be auto-vectorized by conventional general purpose compilers
Splyce recognizes these patterns and converts them into vectorized "fast lanes" using SIMD operations with masked execution and Scalar Epilogue as a dual-path strategy.
- 2-Way Coiteration Vectorization: Vectorizes pointer-chasing loops in sparse tensor operations
- SIMD Fast-Lane Generation: Creates masked SIMD execution paths for coordinate matching
- Scalar Epilogue: Keep the previous scalar coiteration loop as an epilogue.
- Configurable Vector Width: Supports flexible SIMD width selection (4, 8, 16, etc.)
- Phase-Based Optimization: Selective application of vectorization phases
- Integration with MLIR Pipeline: Works seamlessly with existing sparse dialect lowering
- SpGEMM
- SpMTTKRP
- SpMSpV
- SpMMH
- SpTTSpM
- Other sparse coiteration operations.
- CMake 3.28+
- Ninja
- Git
- Python 3.10+
- C++ Compiler (GCC 13+)
- CPU:
x86_64— everyclanginvocation in this repo (Getting Started, Usage Examples, and Evaluation/experiments/) builds with-march=native, so everything builds and runs on whateverx86_64CPU you actually have.
The paper's reference numbers under Evaluation were measured on AVX-512 hardware specifically.
-march=nativemeans every experiment still runs and produces valid results on anyx86_64CPU, but the numbers it produces reflect your own machine's SIMD capabilities — they aren't meant to be compared directly against the reference CSVs, or across machines, unless yours happens to match closely.
Ubuntu / Debian:
sudo apt-get install -y cmake ninja-build python3 python3-pip git zlib1g-devFedora / RHEL:
sudo dnf install -y cmake ninja-build python3 git zlib-develWithout Root Access:
You can proceed with cmake, python3, and git only.
This project is tested against LLVM 23.0.0git (commit 6a6d432550598a59605ee062bd0e35c9d452c0c5).
Fast (recommended): llvm-project's full history is large, but GitHub lets you fetch a single commit directly, skipping all of it:
git init llvm-project && cd llvm-project
git remote add origin https://github.com/llvm/llvm-project.git
git fetch --depth 1 origin 6a6d432550598a59605ee062bd0e35c9d452c0c5
git checkout FETCH_HEADSimple (slower): clones the full history, then checks out the commit:
git clone https://github.com/llvm/llvm-project.git
cd llvm-project
git checkout 6a6d432550598a59605ee062bd0e35c9d452c0c5Build clang/lld from source (no system clang/lld required):
Because clang itself is being built from source here, it will auto-detect a GCC installation on your system to borrow its C++ standard library/headers for building the runtimes. To pin this to a specific GCC instead of whatever the auto-detection picks, pass its install directory explicitly via -DRUNTIMES_CMAKE_ARGS below --- update the path (/usr/lib/gcc/x86_64-linux-gnu/13) to match your system's GCC location and version.
mkdir build
cmake -S llvm -B build -G Ninja \
-DLLVM_ENABLE_PROJECTS="clang;clang-tools-extra;mlir;lld;openmp" \
-DLLVM_ENABLE_RUNTIMES="all" \
-DCMAKE_BUILD_TYPE=Release \
-DLLVM_TARGETS_TO_BUILD="host;X86" \
-DLLVM_INCLUDE_TESTS=OFF \
-DLLVM_USE_LINKER=bfd \
-DCMAKE_C_COMPILER=gcc \
-DCMAKE_CXX_COMPILER=g++ \
-DLLVM_ENABLE_ASSERTIONS=OFF \
-DCMAKE_INSTALL_PREFIX=$HOME/llvm-install \
-DRUNTIMES_CMAKE_ARGS="-DCMAKE_C_FLAGS=--gcc-install-dir=/usr/lib/gcc/x86_64-linux-gnu/13;-DCMAKE_CXX_FLAGS=--gcc-install-dir=/usr/lib/gcc/x86_64-linux-gnu/13"Build and install:
ninja -C build install
export LLVM_INSTALL=$HOME/llvm-install
export PATH=$LLVM_INSTALL/bin:$PATHVerify LLVM installation:
mlir-opt --versionIf you have a pre-built LLVM installation, simply set:
export LLVM_INSTALL=/path/to/llvm-install
export PATH=$LLVM_INSTALL/bin:$PATH$LLVM_INSTALL here is the same one exported in Step 1 (Option A or B) - CMakeLists.txt derives MLIR_DIR/LLVM_DIR from it automatically, so you don't need to pass either explicitly unless you want to point at a different LLVM install than $LLVM_INSTALL (pass -DMLIR_DIR=... -DLLVM_DIR=... to override). CMakeLists.txt also defaults to clang/clang++ (unless you pass -DCMAKE_C_COMPILER=/-DCMAKE_CXX_COMPILER=, or have CC/CXX set) and to a Release build (unless you pass -DCMAKE_BUILD_TYPE=...). Also set --gcc-install-dir below to the same GCC install you built LLVM's runtimes against earlier, so Splyce links against matching C++ standard library headers/libs.
cd /path/to/splyce
cmake -S . -B build -G Ninja \
-DCMAKE_C_FLAGS="--gcc-install-dir=/usr/lib/gcc/x86_64-linux-gnu/13" \
-DCMAKE_CXX_FLAGS="--gcc-install-dir=/usr/lib/gcc/x86_64-linux-gnu/13"
ninja -C build./build/bin/splyce-opt --help | grep splyceOnce you've built Splyce (Building and Installation), the fastest way to see it work is Quick Start below - one command compiles playground/spgemm.mlir both ways, generates its input data if needed, runs both binaries, and prints the speedup. Usage Examples then walks through what that command does under the hood, one pipeline stage at a time, for when you need to inspect or modify the IR yourself.
./playground/run.sh spgemm singleThis compiles playground/spgemm.mlir two ways - a plain mlir-opt --sparsifier baseline and a Splyce-vectorized version (vector-width 4, phase-select 001) - generates tensor_B.tns/tensor_C.tns in playground/ if they don't already exist, runs both binaries once each, and prints:
Kernel: spgemm
Mode: single
Baseline (s): <t>
Splyce (s): <t>
Speedup (x): <t>
For the OpenMP-parallel pipeline instead (Usage Examples, Example 2), swap single for multicore:
./playground/run.sh spgemm multicore [cores]Baseline and Splyce are both compiled with --parallelization (mlir-opt --sparsifier has no OpenMP-aware lowering, so the baseline goes through splyce-opt too here — just without --splyce in between) and run pinned to the same core count, so the printed speedup isolates vectorization's contribution rather than mixing in threading. [cores] defaults to every CPU on NUMA node 0 (override via the NUMA_NODE env var) when numactl is available, else nproc.
Prerequisites: same as Building and Installation — LLVM_INSTALL set, mlir-opt/mlir-translate/clang on $PATH, and build/bin/splyce-opt/build/bin/splyce-translate already built. multicore mode additionally needs clang able to find libomp for -fopenmp to link.
Regenerating input data: run.sh only generates tensor_B.tns/tensor_C.tns when they're missing - it won't overwrite ones you already have. To use a different size/sparsity, generate them yourself first (this overwrites any existing tensors):
./playground/gen_data.sh [dimension] [sparsity]Defaults to a 5000×5000 matrix at 95% sparsity if both are omitted - i.e. ./playground/gen_data.sh on its own is equivalent to ./playground/gen_data.sh 5000 0.95.
A typical Splyce evaluation workflow:
- Sparse Dialect -> SCF (Sparsification)
- SCF -> Vectorized SCF (Splyce Pass)
- Vectorized SCF -> LLVM Dialect (Lowering)
- LLVM Dialect -> Binary (Compilation)
Steps 1–3 can be run either as a single splyce-opt command, or as the
individual mlir-opt / splyce-opt stages it's built from. Both examples
below show both ways; pick whichever suits your workflow - a single
splyce-opt invocation for everyday use, or the broken-out stages when you
need to inspect or modify the IR between passes.
splyce-opt's pipeline-bundling flags:
--sparsify-to-scf: Runs the full linalg/sparse_tensor -> scf lowering pipeline (themlir-optstage below) in one flag.--lower-to-llvm: Runs the full scf -> LLVM dialect lowering pipeline (the othermlir-optstage below) in one flag.--parallelization: Add to either of the above to switch to the OpenMP/parallel pipeline variant instead of the default single-threaded one.--splyce="...": The vectorization pass itself (unchanged either way).
./build/bin/splyce-opt ./playground/spgemm.mlir \
--sparsify-to-scf \
--splyce="target-function=spgemm vector-width=4 phase-select=001" \
--splyce-bufferize-restrict \
--lower-to-llvm \
-o ./playground/spgemm_llvm.mlirSplyce Options:
target-function=<name>: Target function to optimizevector-width=<width>: SIMD vector width (4, 8, 16, etc.)phase-select=<phases>: Bitmask for optimization phases
Continue with 1.4 Generate Binary below.
1.1 Lower Sparse Tensor to SCF
mlir-opt ./playground/spgemm.mlir \
--linalg-generalize-named-ops \
--linalg-fuse-elementwise-ops \
--pre-sparsification-rewrite \
--empty-tensor-to-alloc-tensor \
--sparse-reinterpret-map \
--sparsification \
--stage-sparse-ops \
--lower-sparse-ops-to-foreach \
--lower-sparse-foreach-to-scf \
--loop-invariant-code-motion \
--sparse-tensor-conversion \
-o ./playground/spgemm_scf.mlir1.2 Apply Splyce Vectorization
./build/bin/splyce-opt ./playground/spgemm_scf.mlir \
--splyce="target-function=spgemm vector-width=4 phase-select=001" \
--splyce-bufferize-restrict \
-o ./playground/spgemm_splyce.mlirSplyce Options:
target-function=<name>: Target function to optimizevector-width=<width>: SIMD vector width (4, 8, 16, etc.)phase-select=<phases>: Bitmask for optimization phases
1.3 Lower SCF to LLVM Dialect
mlir-opt ./playground/spgemm_splyce.mlir \
--canonicalize \
--cse \
--loop-invariant-code-motion \
--one-shot-bufferize="bufferize-function-boundaries=true allow-return-allocs-from-loops=true" \
--convert-bufferization-to-memref \
--lower-vector-mask \
--convert-vector-to-scf \
--canonicalize \
--cse \
--expand-realloc \
--sparse-storage-specifier-to-llvm \
--convert-linalg-to-loops \
--lower-affine \
--canonicalize \
--cse \
--convert-scf-to-cf \
--expand-strided-metadata \
--finalize-memref-to-llvm \
--convert-vector-to-llvm \
--convert-math-to-llvm \
--convert-arith-to-llvm \
--convert-func-to-llvm \
--convert-cf-to-llvm \
--reconcile-unrealized-casts \
-o ./playground/spgemm_llvm.mlirBoth options produce the same ./playground/spgemm_llvm.mlir.
1.4 Generate Binary
./build/bin/splyce-translate ./playground/spgemm_llvm.mlir \
--mlir-to-llvmir \
-o ./playground/spgemm_splyce.ll
clang -O3 ./playground/spgemm_splyce.ll \
-march=native \
-fno-vectorize -fno-slp-vectorize \
-L"$LLVM_INSTALL/lib" \
-lmlir_c_runner_utils \
-lmlir_runner_utils \
-Wl,-rpath,"$LLVM_INSTALL/lib" \
-o ./playground/test_benchmark_spgemm_splyce1.5 Run Binary
The binary reads its input tensors from tensor_B.tns and tensor_C.tns in its working directory. Generate them first:
python3 ./playground/gen_data.pyThis writes tensor_B.tns and tensor_C.tns into playground/, regardless of where you run it from.
Then run the binary from playground/, since it looks for those files relative to its own working directory:
cd playground && ./test_benchmark_spgemm_splyce1.6 Compare with Baseline
Baseline binary can be generated from either with mlir-opt or using splyce-opt.
Using mlir-opt:
mlir-opt ./playground/spgemm.mlir \
--sparsifier -o ./playground/spgemm_baseline.mlir
mlir-translate ./playground/spgemm_baseline.mlir --mlir-to-llvmir -o ./playground/spgemm_baseline.llUsing splyce-opt with the Splyce pass disabled:
./build/bin/splyce-opt ./playground/spgemm.mlir \
--sparsify-to-scf \
--splyce-bufferize-restrict \
--lower-to-llvm \
-o ./playground/spgemm_baseline.mlir
./build/bin/splyce-translate ./playground/spgemm_baseline.mlir \
--mlir-to-llvmir -o ./playground/spgemm_baseline.llGenerate the binary from Clang:
clang -O3 ./playground/spgemm_baseline.ll \
-march=native \
-fno-vectorize -fno-slp-vectorize \
-L"$LLVM_INSTALL/lib" \
-lmlir_c_runner_utils \
-lmlir_runner_utils \
-Wl,-rpath,"$LLVM_INSTALL/lib" \
-o ./playground/test_benchmark_spgemm_baselineand now run the baseline:
cd playground && ./test_benchmark_spgemm_baseline./build/bin/splyce-opt ./playground/spgemm.mlir \
--sparsify-to-scf --parallelization \
--splyce="target-function=spgemm vector-width=4 phase-select=001" \
--splyce-bufferize-restrict \
--lower-to-llvm --parallelization \
-o ./playground/spgemm_llvm_parallel.mlirContinue with 2.4 Generate Parallel Binary below.
2.1 Lower Sparse Tensor to SCF (with parallelization)
mlir-opt ./playground/spgemm.mlir \
--linalg-generalize-named-ops \
--linalg-fuse-elementwise-ops \
--pre-sparsification-rewrite \
--empty-tensor-to-alloc-tensor \
--sparse-reinterpret-map \
--sparsification="parallelization-strategy=dense-outer-loop" \
--stage-sparse-ops \
--lower-sparse-ops-to-foreach \
--sparse-reinterpret-map \
--lower-sparse-foreach-to-scf \
--loop-invariant-code-motion \
--sparse-tensor-conversion \
-o ./playground/spgemm_scf_parallel.mlir2.2 Apply Splyce Vectorization
./build/bin/splyce-opt ./playground/spgemm_scf_parallel.mlir \
--splyce="target-function=spgemm vector-width=4 phase-select=001" \
--splyce-bufferize-restrict \
-o ./playground/spgemm_splyce_parallel.mlir2.3 Lower SCF to LLVM Dialect (with OpenMP)
mlir-opt ./playground/spgemm_splyce_parallel.mlir \
--canonicalize \
--sparsification-and-bufferization \
--sparse-storage-specifier-to-llvm \
--canonicalize \
--convert-linalg-to-loops \
--convert-vector-to-scf \
--expand-realloc \
--convert-scf-to-openmp \
--convert-openmp-to-llvm \
--convert-scf-to-cf \
--expand-strided-metadata \
--lower-affine \
--convert-vector-to-llvm \
--convert-complex-to-standard \
--arith-expand \
--convert-math-to-llvm \
--convert-complex-to-libm \
--convert-vector-to-llvm \
--convert-to-llvm \
--reconcile-unrealized-casts \
-o ./playground/spgemm_llvm_parallel.mlirBoth options produce the same ./playground/spgemm_llvm_parallel.mlir.
2.4 Generate Parallel Binary
-fopenmp always links successfully — clang auto-adds its own runtime lib dir (e.g. lib/<target-triple>/, where libomp.so actually lives when LLVM is built as in Step 1) to the link-time search path automatically. But that dir usually isn't $LLVM_INSTALL/lib (the rpath below is for libmlir_c_runner_utils/libmlir_runner_utils, which are directly under it), so without also rpath'ing libomp.so's real directory, the binary can silently depend on whatever libomp.so (if any) happens to already be on the machine you run it on, instead of the one it was actually built against — and fail outright with "cannot open shared object file" on a machine with no system-wide libomp at all (e.g. a fresh Docker container):
./build/bin/splyce-translate ./playground/spgemm_llvm_parallel.mlir \
--mlir-to-llvmir \
-o ./playground/spgemm_splyce_parallel.ll
clang -O3 ./playground/spgemm_splyce_parallel.ll \
-march=native \
-fopenmp \
-fno-vectorize \
-fno-slp-vectorize \
-L"$LLVM_INSTALL/lib" \
-lmlir_c_runner_utils \
-lmlir_runner_utils \
-Wl,-rpath,"$LLVM_INSTALL/lib" \
-Wl,-rpath,"$(dirname "$(clang -print-file-name=libomp.so)")" \
-o ./playground/test_benchmark_spgemm_splyce_parallel2.5 Run Parallel Binary
This binary reads the same tensor_B.tns/tensor_C.tns inputs as the single-threaded example. If you already generated them in 1.5 Run Binary, reuse them as-is; otherwise generate them first:
python3 ./playground/gen_data.pySet OMP_NUM_THREADS to however many threads you want the OpenMP-parallelized loop to use, then run from playground/:
cd playground && OMP_NUM_THREADS=4 ./test_benchmark_spgemm_splyce_parallelFor reliable timing (rather than a quick check), pin the run to real cores on a single NUMA node instead of leaving thread placement to the OS scheduler — e.g.
numactl --physcpubind=0-3 --membind=0 env OMP_NUM_THREADS=4 ./test_benchmark_spgemm_splyce_parallel(ortaskset -c 0-3ifnumactlisn't available). See experiments/multicore/run.sh for the full pinning approach used in the benchmark suite.
Note:
mlir-translate --mlir-to-llvmiris a drop-in equivalent forsplyce-translatein steps 1.4/2.4, if you're already relying on a separate LLVM install being on$PATH.
The Dockerfile at the repo root builds LLVM/MLIR/clang from the pinned commit in Building and Installation, builds Splyce against it, and produces an image that can run everything in Usage Examples out of the box - no local LLVM build required.
Covers the build + Usage Examples workflow only for now — it doesn't (yet) bundle
experiments/(the Evaluation harness).
Build:
docker build -t splyce .Building LLVM from source is the expensive part - expect it to take a while and to need a fair amount of RAM (clang's link step is memory-hungry) and disk space. If the build gets OOM-killed, cap ninja's parallelism:
docker build -t splyce --build-arg NINJA_JOBS=4 .Other supported build args: LLVM_COMMIT (track a different upstream revision) and GCC_MAJOR_VERSION (see Caveats below).
Run:
docker run --rm -it splyceThis drops you into a shell at /splyce with mlir-opt, mlir-translate, clang, splyce-opt, and splyce-translate all on $PATH and $LLVM_INSTALL already set - paste any command from Usage Examples directly (e.g. everything in Example 1: Single-Threaded SpGEMM works as-is; the compiled binary's output is a plain benchmark timing, not anything image-specific).
Non-interactively, the same build already self-checks that splyce-opt works (mirroring Step 3: Verify Build) — you can rerun that check any time with:
docker run --rm splyce ./build/bin/splyce-opt --help | grep splyceCaveats:
- The image is built for
x86_64(gcc-13/ubuntu:24.04, matching the--gcc-install-dirpath used in Building and Installation). Usage Examples'clanginvocations use-march=native, and since Docker doesn't virtualize instruction sets, that resolves against whatever CPU is actually running the container at the time — AVX-512 or not — the same way it would on bare metal. It's stillx86_64-only: an Arm host (e.g. Apple Silicon under Docker Desktop) can't run this image at all, regardless of-march=native. - Unlike the native build in Building and Installation, you shouldn't need to touch the GCC path yourself — the image installs its own
gcc-13/g++-13and derives--gcc-install-dirfrom that automatically, so there's no host-specific path to discover. If you do need a different GCC major version (e.g. theubuntu:24.04repos stop carrying13), pass--build-arg GCC_MAJOR_VERSION=<N>; it drives both the installedgcc-<N>/g++-<N>packages and the derived--gcc-install-dirpath together, so the two can't drift out of sync.
Every experiment lives under the experiments directory, and each one has its own compile.sh/run.sh (or, for the real-world-data kernels, compile.sh/run_suitesparse_benchmark.py) — but all of them are driven from a single entry point, experiments/run.sh.
Prerequisites (in addition to Building and Installation):
LLVM_INSTALLset, withmlir-opt,mlir-translate, andclangon$PATH.build/bin/splyce-optandbuild/bin/splyce-translatealready built.- Every plotting experiment (anything other than
table2/realdata) needsmatplotlib. Set up a virtual environment once before running anything:python3 -m venv venv . venv/bin/activate pip install matplotlib
Run every command below from inside the experiments directory.
experiments has four standalone experiment directories (phase_ablation, vector_width, sparsity_scaling, multicore) plus a speedups/{synthetic_data,real_world_data}/{spgemm,spmmh,spmspv,spmttkrp,spttspm} tree — 14 named experiments in total, each runnable individually by name. All of them can also run in one shot:
./run.sh all # took ~11 hours in our server
allruns every experiment above sequentially (including the SuiteSparse downloads for the_realworldkernels), so it needs network access and can take a while.
Phase ablation is the experiment to find which phase configuration gives the better performance on SpGEMM kernel. Also the same experiment helps to find the TMA numbers.
You can simply run this experiment as follows:
./run.sh phase_ablation # took ~40 minutes in our serverThis will generate two files:
phase_ablation/tma_results.csv- Table 1 - usephase_ablation/reference.csvfor reference.phase_ablation/tma_breakdown_plot.png- Figure 11 - usephase_ablation/reference.pngfor reference.
This experiment automatically generates the required data and will produce the experimental results shown in Table 2 of the submitted paper for all 5 sparse tensor kernels.
You can simply run this experiment as follows:
./run.sh table2 # took ~40 minutes in our serverThis generates a results.csv file inside each kernel's directory under speedups/synthetic_data, plus one combined speedups/synthetic_data/speedup_summary.csv - use speedups/synthetic_data/speedup_summary_reference.csv for reference.
If you want to run just one of the five kernels, you can run it individually instead:
./run.sh spgemm_speedup
./run.sh spmspv_speedup
./run.sh spmttkrp_speedup
./run.sh spmmh_speedup
./run.sh spttspm_speedupOnce every kernel's results.csv exists, python3 speedups/synthetic_data/print_speedup_summary.py will print the combined table to the terminal and (re)write speedup_summary.csv.
In different hardware depending on their capabilities the vector width to get the optimal performance can vary. For the hardware environment we test, we found that vector width 4 seems to show the significant speedup compared to other vector width in different sparsity factors.
You can simply run this experiment as follows:
./run.sh vector_width # took ~1.5 hours in our serverThis will generate two results files:
vector_width/results.csvcontains all the execution numbers - usevector_width/reference.csvfor reference.vector_width/vector_width_speedup_plot.png- Figure 12 - usevector_width/reference.pngfor reference.
This experiment is to show that Splyce's performance increase with increasing non zero density and even in extreme sparse data, Splyce does not show any significant degradation in performance.
You can simply run this experiment as follows:
./run.sh sparsity_scaling # took ~30 minutes in our serverThis will generate two results files:
sparsity_scaling/results.csvcontains all execution numbers - usesparsity_scaling/reference.csvfor reference.sparsity_scaling/sparsity_scaling_plot.png- Figure 13 - usesparsity_scaling/reference.pngfor reference.
This experiment is to show the performance speedup Splyce produce for single core is linearly propotional to its parallel core execution. This ensure all the performance improvement are pinned to a single core and use multiple cores gives the same amount of performance as number of cores being used.
You can simply run this experiment as follows:
./run.sh multicore # took ~15 minutes in our serverThis will generate two results files:
multicore/results.csvcontains all execution numbers - usemulticore/reference.csvfor reference.multicore/speedup_plot.png- Figure 14 - usemulticore/reference.pngfor reference.
The parallel variant needs LLVM built with
-DLLVM_ENABLE_RUNTIMES=openmpso thatlibomp.sois present under$LLVM_INSTALL/lib;multicore/run.shdetects its absence and skips the parallel variant automatically.
This is to show that Splyce not only gives performance on synthetic data but also on real-world matrices from the SuiteSparse matrix library. Matrices are not included in the repository - the script downloads each one it needs on demand (and, the first time any _realworld kernel runs, one-time scrapes speedups/real_world_data/suitesparse/matrix_metadata.json for download URLs).
You can simply run this experiment as follows:
./run.sh realdata # took ~7-8 hours in our serverThis downloads the required matrices, runs them on all five kernels, and produces speedups/real_world_data/realworld_summary.csv - compare against speedups/real_world_data/realworld_summary_reference.csv for reference.
If you want to run just one of the five kernels, you can run it individually instead:
./run.sh spgemm_realworld
./run.sh spmspv_realworld
./run.sh spmttkrp_realworld
./run.sh spmmh_realworld
./run.sh spttspm_realworldOnce every kernel's <kernel>_realworld_results.csv exists, python3 speedups/real_world_data/print_realworld_summary.py will print the combined table to the terminal and (re)write realworld_summary.csv.
Error: "mlir-opt not found"
# Ensure LLVM_INSTALL is set and PATH is updated
export PATH=$LLVM_INSTALL/bin:$PATH
which mlir-optError: "MLIR_DIR or LLVM_DIR not found"
# Verify CMake paths
ls $LLVM_INSTALL/lib/cmake/mlir
ls $LLVM_INSTALL/lib/cmake/llvm
# If missing, rebuild LLVM with correct installation prefixError: "Ninja: build halted"
# Check for missing dependencies (zlib, python3, etc.)
# Rebuild with verbose output
ninja -C build -v 2>&1 | tail -50Error: "splyce pass crashed on function X"
- Check that the target function exists:
mlir-opt input.mlir --print-op-graph - Verify IR is in SCF dialect (after sparsification lowering)
- Add debug output:
./build/bin/splyce-opt input.mlir --splyce=... -debug 2>&1 | head -100
Error: "Vector width not supported"
# Use a standard vector width (4, 8, 16)
# Verify your CPU supports the target SIMD width with:
grep avx512f /proc/cpuinfo # AVX-512
grep avx /proc/cpuinfo # AVX2Performance worse after vectorization
- Check sparsity density: if <1%, vectorization overhead may hurt
- Try different vector widths
- Compare generated LLVM IR:
cat ./spgemm_splyce.ll | grep -A5 -B5 mask
Inspect generated IR at each stage:
# After sparsification
mlir-opt input.mlir --print-op-graph > scf.txt
# After Splyce vectorization
./build/bin/splyce-opt input.mlir --splyce=... --print-op-graph > vectorized.txt
# Diff the IR
diff -u scf.txt vectorized.txt | head -50Profile the generated binary:
# With performance counters (Linux)
perf stat ./test_benchmark_spgemm_splyce
# With VTune (if installed)
vtune -collect hotspots -r ./results -- ./test_benchmark_spgemm_splyceAnalyze instruction distribution:
llvm-objdump -d ./spgemm_splyce.ll | grep vmul | wc -l
llvm-objdump -d ./spgemm_splyce.ll | grep vpadd | wc -lSplyce is licensed under the Apache License 2.0 with LLVM Exceptions, matching the license of the MLIR/LLVM infrastructure it builds on. See LICENSE for the full text.