Tutorial 1 — Running the Transactional Emulator¶
This tutorial walks through compiling a single ATen-style operator (a Linear layer) all the way down to PLENA machine code and executing it on the Rust transactional emulator that ships with PLENA_Simulator.
By the end you will have:
- Generated PLENA ISA for
Y = X @ Wfrom a Python program. - Run it through the cycle-approximate emulator (with Ramulator / DRAMSys modelling off-chip HBM).
- Compared the emulator output against a CPU golden reference.
The emulator is cycle-approximate, not cycle-accurate. See the Transactional Emulator page for what it does and does not model.
1. Prerequisites¶
You need a working PLENA_Simulator install (see Getting Started — Section 2).
Confirm the environment is live:
cd PLENA_Simulator
direnv allow # If you have not already
just --list | head # Should print the recipes from the justfile
The first time you enter the shell, the Rust crate is built. To pre-build the emulator binary so the first run is faster:
Make sure the [MODE] section of plena_settings.toml at the repo root is
set to transactional so the precision settings the testbench loads match
what the emulator expects:
2. Run the canonical Linear test¶
From the repo root:
That single recipe drives the full flow shown below.
┌──────────────────────────┐ PLENA ISA ┌────────────────────────────┐
│ linear_test.py │ ─────────────▶│ transactional_emulator │
│ • build CPU golden │ memory │ • Rust, cargo --release │
│ • compile via │ images │ • Ramulator / DRAMSys │
│ PlenaCompiler │ ─────────────▶│ • dumps VRAM contents │
└──────────────────────────┘ └──────────────┬─────────────┘
│
▼
┌────────────────────────┐
│ run_and_assert() │
│ numerical comparison │
└────────────────────────┘
What just test-aten-linear actually does¶
Internally, the recipe runs
transactional_emulator/testbench/aten/linear_test.py, which:
- Builds a random
(batch=64, in=128)activationXand(128, 256)weightW. - Computes a CPU golden
Y = X @ Wvia theOpRegistryset toBackend.CPU. - Switches the registry to
Backend.PLENAand re-runsops.linear(prog, …)— this dispatches to a PLENA-specific lowering that emits ISA intoPlenaCompiler. - Writes the generated ASM, HBM image, FP/INT SRAM images and a
comparison_params.jsontotransactional_emulator/testbench/aten/build/linear/. - Invokes the Rust emulator on those memory images via
run_and_assert(build_dir, "linear", mlen, blen), which compares the emulator's VRAM dump against the golden tensor.
A successful run ends with a numeric-match line and exit code 0.
3. Inspect the generated artefacts¶
After the run, look under
transactional_emulator/testbench/aten/build/linear/:
| File | Purpose |
|---|---|
generated_asm_code.asm |
Human-readable PLENA assembly produced by PlenaCompiler.compile(). |
generated_machine_code.mem |
Hex machine code loaded into the emulator's instruction memory. |
hbm_for_behave_sim.bin |
Initial HBM image (weights + activations packed in MX precision). |
fp_sram.bin, int_sram.bin |
Pre-initialised on-chip SRAM (FP constants + integer fast paths). |
comparison_params.json |
Tells run_and_assert where Y lives in VRAM and how to reshape it. |
To eyeball the ASM, open generated_asm_code.asm and look for
H_PREFETCH_M / H_PREFETCH_V / M_MAC / H_STORE_V — these are the
instruction classes the emulator schedules. The
ISA Specification explains each one.
4. Run a different operator¶
Several other ATen-style operators are wired up the same way:
just test-aten-softmax # fp variant softmax
just test-aten-rms-norm # RMSNorm
just test-aten-layer-norm # LayerNorm
just test-aten-ffn # 2-layer FFN
just test-aten-flash-attention # on-chip FlashAttention GQA
just test-aten-bmm # batched matmul (direct-emit path)
just test-aten-rope # RoPE position embedding
just test-aten-embedding-add # token + position embedding add
just test-aten-conv2d # 2D conv (sweeps several presets)
Each *_test.py follows the same three-phase structure: build CPU golden →
compile via PlenaCompiler → execute on the emulator and diff. Use them as
templates for new operators.
5. Run a full model on the emulator¶
For multi-layer / multi-step runs, use the unified driver:
# Compile-only (no emulation), useful to inspect ISA size
just aten-compile smollm2 --config sliced_64x64x16_b1
# Compile + emulate
just aten-emulate llada-8b --config native_256x256x64_b1
just aten-emulate smolvlm2 --case vision-layers --layers 5
Model nicknames and per-model configs live under
transactional_emulator/testbench/configs/ (YAML files). The driver picks
the right ASM templates, memory layout, and precision settings for the model
you ask for.
6. Run a low-level test that exercises the emulator directly¶
The just build-emulator <arg> recipe skips the compiler path and runs one
of the hand-written testbenches under
transactional_emulator/testbench/. It also dumps post-run memory:
just build-emulator <testbench_name> # release, --quiet
just build-emulator-debug <testbench_name> # verbose tracing
The recipe rebuilds the workload, calls the Rust binary with
--opcode / --hbm / --fpsram / --intsram pointing at the freshly generated
images, and finishes with PLENA_Tools/verification/view_mem.py so you can
read the result memory in tensor form.
7. Customising the run¶
- Change the hardware sizes: edit
plena_settings.toml→[TRANSACTIONAL.CONFIG.*](BLEN, MLEN, VLEN, HLEN, HBM geometry, per-instruction latencies). The testbench reloads these on every run. - Change the workload sizes: each
*_test.pyreads its parameters viagi("name", default). You can override defaults from the GUI or by editing the file. - Toggle MX format: precision settings (MXFP vs MXINT, exponent / scale
width) live under
[TRANSACTIONAL.PRECISION.*]inplena_settings.toml.
8. Troubleshooting¶
error: linker 'cc' not found or libtorch errors
You are not inside the Nix shell. Run direnv allow from the repo root, or
nix develop.
Test fails with a large numerical mismatch
Re-run with just build-emulator-debug linear (substitute the workload
you're testing) to get cycle-by-cycle traces. Mismatches are usually caused
by precision settings in plena_settings.toml not matching what the
testbench expects.
Cargo rebuilds from scratch every time
The build-emulator recipe deletes transactional_emulator/build (the
workload outputs), not Cargo's target/ cache. If target/ is being wiped,
something is invalidating the build script — check that LIBTORCH /
LIBTORCH_USE_PYTORCH are still set after entering the shell.
Next steps¶
- Tutorial 2 — Running the Analytic Model — the fast closed-form cost estimator for the same configurations.
- Tutorial 3 — Running an RTL Simulation — drive the full SystemVerilog design with the same workload.
- Transactional Emulator — design rationale and what the emulator models.
- ISA Specification — the instructions you see in
generated_asm_code.asm.