macOS: Building and Simulator-Based Runtime Testing
This page covers developing tt-lang on an Apple Silicon Mac: building the
LLVM/MLIR toolchain natively, provisioning the hardware simulator, and running
tt-lang’s runtime (REQUIRES: tt-device) tests against that simulator inside a
Linux VM. Runtime tests need tt-metal, which is Linux-only, so they run in a
Lima VM rather than on the macOS host.
Two different “simulators”
Do not conflate them:
Functional simulator |
Hardware simulator |
|
|---|---|---|
Name |
|
ttsim – |
What it is |
tt-lang ops as pure Python (torch-backed) |
Simulates the Tensix/RISC-V device; loaded by tt-metal via |
Needs tt-metal? |
No |
Yes |
Runs on macOS host? |
Yes (native) |
No – needs Linux (Lima VM) |
Docs |
this page |
This page is about the hardware simulator. For tt-lang-sim, see
simulator.md.
macOS host toolchain build (LLVM/MLIR only)
On macOS, scripts/build-and-install.sh builds LLVM/MLIR and the
Python venv but skips tt-metal (Linux-only runtime dependencies). This is enough
for compile-only tests and MLIR lit tests on the host; runtime tests need the VM
(below).
# The default python3 may be too new for tt-metal/ttnn; use a supported one.
# Ensure the install prefix is writable (see build.md).
sudo mkdir -p /opt/ttlang-toolchain && sudo chown "$USER" /opt/ttlang-toolchain
./scripts/build-and-install.sh --toolchain-only --python "$(brew --prefix)/bin/python3.12"
Notes:
Python 3.10+ is required (3.12 recommended); pin an installed supported interpreter with
--pythonsince the systempython3is often newer than tt-metal/ttnn support.The macOS build produces an LLVM/MLIR-only toolchain (
mlir-opt,llvm-lit,MLIRConfig.cmake, and the venv under/opt/ttlang-toolchain).
macOS portability fixes in build-and-install.sh
Three changes make the script work correctly on macOS (/bin/bash 3.2, BSD
coreutils):
set -Eeo pipefail+ anERRtrap so any failure exits non-zero with a message (a failure can never be reported as success). Note a caller that pipes output throughteestill masks the code – invoke without a masking pipe.df -BM->df -h(GNU-only flag; BSDdferrors on-BM).The
do_finalizenormalize + cleanup steps run only on Linux. They are CI/Docker packaging (relocatable install; stub non-essential LLVM binaries to slim the image) and rely on bash 4 (mapfile,declare -A) and GNU tools (chmod --reference,strip --strip-unneeded); on macOS the local toolchain is used in place, so they are skipped.
The hardware simulator (libttsim.so)
tt-metal loads a libttsim.so (built for a specific chip) via
TT_METAL_SIMULATOR. The runtime-test setup below stages one automatically with
test/hw-sim/vm-install-sim.sh:
Default (public ttsim): download a pinned
tenstorrent/ttsimrelease for the chip and host architecture. ttsim publishes Linux/aarch64 assets (e.g.libttsim_wh_aarch64.so), so Apple Silicon needs no sim build. Pin the release withTTSIM_VERSIONand pick the chip withTTSIM_CHIP=wh|bh.Custom simulator: to use an internal or locally-modified simulator, set
TTLANG_SIM_SO=<path>to a prebuiltlibttsim.so, orTTLANG_SIM_SRC=<dir>to a simulator source tree that the script builds with./make.py.
Either way it stages libttsim.so and the matching tt-metal SOC descriptor at
/opt/ttlang-toolchain/sim/. A downloaded release must be ABI-compatible with the
tt-metal built into the toolchain; see the
libttsim API.
Building a custom simulator from source
A simulator source tree builds its .so with ./make.py (what TTLANG_SIM_SRC
runs):
cd <simulator-src>
./make.py src/_out/release_wh/libttsim.so src/_out/release_bh/libttsim.so
Build only the
.sotargets – not./make.py :build, which pulls intests/:buildand requires the SFPI RISC-V cross-compiler (Linux-only).The
.sois architecture- and OS-specific: build it where it runs (the aarch64 VM). A macOS build is an arm64 Mach-O that only loads on macOS – useful for a standalone compile/smoke check (./make.py tests/_out/rv32_alu, thentests/_out/rv32_alu --sim src/_out/release_wh/libttsim.so --loops 0 1000).Apple clang (
-Werror) may flagstatic inlinehelpers that GCC (Linux) does not; annotate them[[maybe_unused]]. Such patches are macOS-host-only.
Runtime tests in a Lima VM
Lima runs an Ubuntu aarch64 VM natively on Apple Silicon.
tt-metal builds and runs there, so tt-lang’s REQUIRES: tt-device tests execute
against a libttsim.so with no silicon. tt-mlir is not needed.
Everyday work stays in your single checkout (~/tt/tt-lang): tt-lang builds
directly from the mounted source into build-lima/, and tests run there. Only the
one-time toolchain build (LLVM + tt-metal) needs a VM-local scratch copy –
tt-metal writes firmware ELFs and its CPM cache into its own source tree, and
virtiofs rejects those writes – and that copy is internal to
vm-build-toolchain.sh (reclaimed when it finishes); you never work in it.
Helper scripts live in the repo at test/hw-sim/ (ttsim-vm.yaml,
vm-build-toolchain.sh, vm-install-sim.sh, vm-build-ttlang.sh,
vm-resume-build.sh). They discover the mount by finding the dir that contains
tt-lang (override with SRC_HOST).
Prerequisites: Homebrew, brew install lima, and ~80 GB free host disk. The
toolchain build tree lands on the VM’s disk image, which draws from host free
space and is not reclaimed by deleting files inside the VM (see disk note).
aarch64 gotchas the scripts handle
tt-metal’s
install_dependencies.shdownloads an amd64-onlyopenmpi-ulfm.deb; run it with--no-distributed(single-device sim needs no MPI).A wrong-arch x86-64
cmakein/usr/local/bincan shadow the apt arm64 one; remove it.install_dependencies.shinstalls Clang from apt.llvm.org as a versioned package (e.g.clang-20); tt-lang’s CMake wants bareclang/clang++, so register the newest installedclang-Nwithupdate-alternatives.The toolchain build runs from a VM-local copy, not the mount: tt-metal writes into its own tree and those writes fail over virtiofs. tt-lang itself builds fine over virtiofs, so
build-limauses your real checkout directly.
One-time setup
Run from the tt-lang repo root on the host. tt-lang lives under a TT root
that the VM mounts – default ~/tt, set by the mounts: entry in
test/hw-sim/ttsim-vm.yaml (change it if your clone lives elsewhere).
# 1. Check out tt-metal and its submodules (umd, tracy) so the build finds them
# (works even if the repo was cloned without --recurse-submodules).
git submodule update --init --recursive --depth 1 third-party/tt-metal
# 2. Create the VM (Ubuntu 24.04 aarch64). Config path is relative to the repo root.
limactl start --tty=false --name=ttlang-ttsim test/hw-sim/ttsim-vm.yaml
# Discover where the TT root (the dir holding tt-lang) is mounted in the guest:
TT="$(limactl shell ttlang-ttsim -- bash -c 'findmnt -nrt virtiofs -o TARGET | while read m; do [ -d "$m/tt-lang" ] && { echo "$m"; break; }; done')"
# 3. Build the toolchain (LLVM + tt-metal) into /opt/ttlang-toolchain. Detached, so
# it survives a dropped SSH session; logs to the mount (host-visible).
limactl shell ttlang-ttsim -- bash -c \
"setsid bash $TT/tt-lang/test/hw-sim/vm-build-toolchain.sh > $TT/tt-lang/toolchain-build.log 2>&1 </dev/null & disown"
# watch on the host: tail -f toolchain-build.log
# 4. Install the simulator into the toolchain. Default: download a pinned public
# ttsim release. For a custom simulator, prefix TTLANG_SIM_SRC=<dir> (build from
# source) or TTLANG_SIM_SO=<path> (prebuilt); TTSIM_CHIP defaults to wh.
limactl shell ttlang-ttsim -- bash "$TT/tt-lang/test/hw-sim/vm-install-sim.sh"
# 5. Auto-set the simulator env for every interactive shell.
limactl shell ttlang-ttsim -- bash -c \
'grep -q TT_METAL_SIMULATOR ~/.bashrc || printf "\nexport TT_METAL_SIMULATOR=/opt/ttlang-toolchain/sim/libttsim.so\nexport TT_METAL_SLOW_DISPATCH_MODE=1\n" >> ~/.bashrc'
Build and test tt-lang (one checkout, build-lima)
Everything below uses your single tree; edit tt-lang on the host and it is live in
the VM via the mount. limactl shell ttlang-ttsim opens an interactive shell,
just like docker exec -it.
limactl shell ttlang-ttsim
cd "$(findmnt -nrt virtiofs -o TARGET | while read m; do [ -d "$m/tt-lang" ] && { echo "$m"; break; }; done)/tt-lang"
# Build tt-lang from this source into build-lima/, against the prebuilt toolchain
# (once to configure, incremental thereafter). Or: bash test/hw-sim/vm-build-ttlang.sh
cmake -G Ninja -B build-lima -DTTLANG_USE_TOOLCHAIN=ON -DTTLANG_TOOLCHAIN_DIR=/opt/ttlang-toolchain
cmake --build build-lima
source build-lima/env/activate # venv + paths for this build
# TT_METAL_SIMULATOR + slow-dispatch are already set via ~/.bashrc
# Run tests (all against your checkout):
python -m pytest test/python/pipe/test_broadcast_2d.py -xvs # a pytest
python -m pytest test/me2e/test_compute_ops.py -k add -xvs # me2e
llvm-lit -v build-lima/test/python/dram_interleaved_add.py # a python lit test
Remember the split: pytest runs against test/..., lit against
build-lima/test/... (the generated lit.site.cfg.py lives in the build dir, so
llvm-lit test/... on the source tree fails with AttributeError: ... python_executable).
Parallel test runs
check-ttlang-pytest and check-ttlang-me2e append -n ${TTLANG_SIM_PYTEST_JOBS}
to pytest when TT_METAL_SIMULATOR is defined at configure time – the signal
that this build targets the simulator. An interactive shell exports it via
~/.bashrc and vm-build-ttlang.sh sets it explicitly, so configuring through
either enables parallelism; a bare cmake in a shell that lacks it configures the
targets serially. Each worker starts its own libttsim.so device (~2-3 GiB), so
the default of 2 suits the 16 GiB reference VM; raise it (with the VM’s memory:)
via -DTTLANG_SIM_PYTEST_JOBS=<N>.
ninja -C build-lima check-ttlang-me2e # runs pytest -n 2 under the sim
Operational notes
Detach long builds (
setsid ... & disown) and log to the mount for host visibility; a droppedlimactl shellotherwise orphans the build. The toolchain build is the long one; the incrementalbuild-limabuild is short.Disk: the VM diffdisk grows with the toolchain build and is not returned to the host when files are deleted inside the VM (
fstrimdid not reclaim it here). If it bloats across failed attempts, delete and recreate the VM.Post-uplift: after bumping the LLVM/tt-metal submodules, re-init tt-metal’s submodules (step 1), remove
/opt/ttlang-toolchain(toolchain reuse is keyed on file existence, not the submodule SHA – see build.md), rerunvm-build-toolchain.sh, rerunvm-install-sim.sh(bumpTTSIM_VERSIONif the new tt-metal needs a newer ttsim ABI), then rebuildbuild-lima.
Setting TT_METAL_SIMULATOR is what enables the runtime tests: tt-lang’s
test/lit.cfg.py adds the tt-device lit feature when TT_METAL_SIMULATOR is set
(or when hardware is present), so REQUIRES: tt-device tests run under the
simulator. See testing.md.
Reference: how tt-mlir CI uses the same simulator
tt-mlir’s CI uses the same public-ttsim mechanism that vm-install-sim.sh
automates: download a prebuilt libttsim_{wh,bh}.so from tenstorrent/ttsim
releases (Linux/aarch64 assets are suffixed _aarch64, e.g.
libttsim_wh_aarch64.so), place it beside the matching tt-metal SOC descriptor
(wormhole_b0_80_arch.yaml / blackhole_140_arch.yaml) as soc_descriptor.yaml,
and set TT_METAL_SIMULATOR + TT_METAL_SLOW_DISPATCH_MODE=1. tt-mlir
additionally runs ttrt query --save-artifacts to produce system_desc.ttsys
and passes --sys-desc to its gated tests.
Status
Verified end-to-end on a fresh Apple Silicon Lima VM (Ubuntu 24.04 aarch64):
current LLVM + tt-metal build from the submodules, tt-lang builds against that
toolchain in build-lima, and pytest / me2e / lit device tests (e.g.
dram_interleaved_add) execute under TT_METAL_SIMULATOR and pass – against
both the default public ttsim release and a custom simulator built from source
via TTLANG_SIM_SRC.