How to Build a Vitis AI Engine (AIE) Graph
Goal: Compile an AI Engine graph from C++ sources, package the resulting
libadf.a against a Vivado-built .xsa into a dynamic PDI with a matching
partition.conf sidecar, and (optionally) deploy the pair to /boot/aie/
on a Versal/PetaLinux board.
Note
This backend (system_vitis_unified_aie.mk) requires the Vitis 2025.1 or
newer unified toolchain. It is driven by the Vitis Python API
(vitis -s <script>.py) — mirroring How to Generate a Vitis HLS IP Core’s unified backend so
the same workspace can be opened batch-style for CI and GUI-style for
debugging.
Prerequisites
vitisbinary on PATH (Vitis 2025.1+ unified toolchain)XILINX_VITISenvironment variable exported (the Makefile asserts on this at parse time)A Vivado-built
.xsafrom the companion target (required bymake packageonly — not bymake proj/make build)An
aie_config.cfgat$(PROJ_DIR)/aie_config.cfg(same convention as the unified HLS flow’shls_config.cfg)Makefile includes
system_vitis_unified_aie.mk
Project Layout
ruckus’s standard convention is to keep the Vivado target and the AIE component in separate project directories (mirroring how Vitis HLS targets are split out from Vivado targets). A typical tree:
firmware/
submodules/ruckus/
shared/
<AieProject>/
Makefile <- includes system_vitis_unified_aie.mk
aie_config.cfg <- [aie] pl-freq=…, Xpreproc=…
aie/
graph.cpp
graph.h
kernels/
*.cc *.h
targets/
<VivadoTarget>/
Makefile <- includes system_vivado.mk; produces the .xsa
The AIE project is fully self-contained: make writes the final
deliverable pair — $(PROJECT).pdi + $(PROJECT).partition.conf — to
$(PROJ_DIR)/ip/ (matching the HLS convention of writing deliverables
into ip/) so the Vivado target does not need to know where the AIE
archetype lives on disk.
Makefile include line:
include $(TOP_DIR)/submodules/ruckus/system_vitis_unified_aie.mk
A minimal consumer Makefile looks like:
export AIE_SOURCES = \
$(abspath $(CURDIR)/aie) \
$(abspath $(CURDIR)/aie/kernels):kernels
export AIE_TOP_LEVEL_FILE = graph.cpp
# Wildcard picks up the xpfm the active Vitis release ships
# (xilinx_vek280_base_202510_1 in 2025.1, _202520_1 in 2025.2, …)
export AIE_PLATFORM := $(firstword $(wildcard \
$(XILINX_VITIS)/base_platforms/xilinx_vek280_base_*/xilinx_vek280_base_*.xpfm))
# Directory of Vivado-built .xsa images; `make package` auto-selects
# the newest by the BUILD_TIME timestamp encoded in the filename.
export VIVADO_XSA_DIR = $(abspath $(CURDIR)/../../targets/<VivadoTarget>/images)
AIE_BOARD_IP ?= root@10.0.0.191
include ../../submodules/ruckus/system_vitis_unified_aie.mk
No target: rule is needed — the include provides the default chain
target: package partition_conf (build → package → emit the
partition.conf sidecar), so a bare make produces the full
ip/ deliverable pair.
Steps
Create the workspace and AIE component:
make projIdempotent — re-running is a cheap no-op if the component already exists, so
make buildandmake guideclareprojas a prereq freely.Build the graph for hardware:
make buildEmits
libadf.aunder$(OUT_DIR)/$(PROJECT)/build/hw/.Or run the x86 simulator instead of the AIE compiler:
make x86simWrap
libadf.a+ the Vivado.xsainto a dynamic PDI:make packageThe
.xsais auto-discovered fromVIVADO_XSA_DIR(set in the consuming Makefile): the newest.xsawins, by theBUILD_TIMEtimestamp encoded in the image filename. A directory is used because the Vivado-sideIMAGENAMEembedsBUILD_TIME, so the.xsafilename produced by an earlier Vivado run is not predictable from inside the AIE archetype.The package step’s output is
$(AIE_PDI)—$(PROJ_DIR)/ip/$(PROJECT).pdiby default. The PDI is a CDO-only partial PDI loaded at runtime through/sys/class/fpga_manager/fpga0/firmware; no device-tree overlay is part of the deliverable (theai_engineDT node is already live from the design’spl.dtbo).Emit the
partition.confsidecar for the/boot/aie/runtime:make partition_confExtracts the AIE partition geometry (
PARTITION_ID+UID) from the Vitis-emittedaie_partition.jsonand writes$(PROJ_DIR)/ip/$(PROJECT).partition.conf— consumed on-board byaie-partition-init. A baremakeruns steps 2, 4, and 5 in one go (default chaintarget: package partition_conf).Deploy the pdi+conf pair to a Versal/PetaLinux board:
make AIE_BOARD_IP=root@<board-ip> program
The default deploy helper (
$(RUCKUS_DIR)/vitis/aie/program.sh) scp’s the PDI andpartition.confto/boot/aie/<name>.{pdi,partition.conf}, reboots the board, waits for ssh liveness, then verifies the startup-app-init boot loop loaded the AIE (journalAIE load:line +fpga_managerdmesg write), thataie-partition-init@<name>.serviceis active, and that/sys/class/aieexists.Open the same workspace interactively in the Vitis IDE:
make guiOr drop into a Python REPL with the
vitismodule pre-loaded:make interactive
Output: deliverable pair at $(PROJ_DIR)/ip/$(PROJECT).pdi +
$(PROJ_DIR)/ip/$(PROJECT).partition.conf.
Available Targets
Target |
Action |
|---|---|
|
Create the workspace + AIE component ( |
|
Build for the |
|
Build for the x86 simulator ( |
|
Wrap |
|
Invoke |
|
Invoke |
|
Open the same workspace in the Vitis Unified IDE
( |
|
Drop into a Python REPL with the |
|
Print all AIE-related environment variables — useful for diagnosing Makefile variable resolution. |
|
Delete the build directory ( |
Caller Contract
The consuming target’s AIE Makefile must define these before including
system_vitis_unified_aie.mk:
Exactly one of:
AIE_PLATFORM— absolute path to the platform.xpfm. Use this for dev boards with an AMD-shipped xpfm (e.g.$(XILINX_VITIS)/base_platforms/xilinx_vek280_base_*/...xpfm).AIE_PART— Versal device ID. Use this for custom AIE boards without a shipped xpfm (e.g.xcve2802-vsvh1760-2MP-e-S). The PL clock hint comes from the cfg’spl-freq=line instead of the xpfm.
Plus:
AIE_SOURCES— whitespace-separated list ofpath[:dest_subdir]entries. Thepathhalf is either:a file path → imports just that file, or
a directory path → imports every
.cpp/.cc/.h/.hppfile in that directory (non-recursive — subdirectories are not walked).
Without
:dest_subdir, imports land flat at the component root. With:dest_subdir, they import into<component>/<dest_subdir>/— needed for the AMD-canonicalkernels/layout wheregraph.hdoesadf::source(loop) = "kernels/loopback.cc";.include=paths for the component root and everydest_subdirare auto-merged into the generated cfg, so headers in those directories resolve without manualXpreproc=-Ilines. This mirrors the trust model of HLS’shls_config.cfg(syn.file=/tb.file=) — the application owns the source list; ruckus does no Python-side recursive globbing.Mix local and shared sources freely:
SHARED_AIE := $(TOP_DIR)/submodules/aie-common-kernels export AIE_SOURCES = \ $(CURDIR)/aie \ $(CURDIR)/aie/kernels:kernels \ $(SHARED_AIE)/util.cc
The first entry pulls in everything directly under
aie/(flat at the component root); the second pulls in everything underaie/kernels/into the component’skernels/subdirectory; the third names one file from a submodule.AIE_TOP_LEVEL_FILE— top-level graph file basename only (e.g.graph.cpp), because the top-level graph imports at the component root.VIVADO_XSA_DIR— directory containing the Vivado-built.xsaimages, required by thepackagetarget (asserted at recipe time, not parse time). The newest.xsa— by theBUILD_TIMEtimestamp encoded in the image filename — is selected automatically; the build hard-errors if the directory contains no.xsa.
And on the command line (recipe-time, not parse-time):
AIE_BOARD_IP—user@hostfor theprogramtarget. Forwarded to$(AIE_PROGRAM_SCRIPT)as the-iflag.AIE_CONF— optional override for thepartition.confsidecar path, forwarded to$(AIE_PROGRAM_SCRIPT)as the-cflag when set. When unset the helper auto-derives it from the PDI directory as<pdi-dir>/<name>.partition.conf— which matches theip/pair written by the defaulttargetchain, so most consumers never set it.
aie_config.cfg
The consuming Makefile must also provide an aie_config.cfg file at
$(PROJ_DIR)/aie_config.cfg — next to the Makefile itself. This
location is hardcoded (not configurable), mirroring the unified HLS flow’s
hls_config.cfg convention. The cfg’s [aie] section carries
pl-freq= and any other aiecompiler options:
[aie]
pl-freq=250
xlopt=1
verbose=true
# Xpreproc=-DMY_MACRO=value
When AIE_PART is in use (no xpfm), the pl-freq= line is the only
hint Vitis has for the PL clock frequency, so it must be set.
The cfg actually attached to the component is a generated file
($(OUT_DIR)/aie_config.generated.cfg) that merges auto-derived
include= lines — the component root plus every AIE_SOURCES
dest_subdir — ahead of the user’s aie_config.cfg body. Edits to
aie_config.cfg are re-merged on every make proj; never edit the
generated file directly.
Customising the Deploy Step
make program invokes $(AIE_PROGRAM_SCRIPT), which defaults to
$(RUCKUS_DIR)/vitis/aie/program.sh — a generic Versal/PetaLinux deploy
helper that:
Derives the normalized
<name>from the-pPDI basename minus.pdi(a legacy_aie_dynamic/_dynamicsuffix is stripped, soAieLoopback_aie_dynamic.pdistill deploys asAieLoopback)Pre-flights the PDI path and ssh reachability, and refuses to upload a
*_static*filename (which belongs inBOOT.BIN, not as a runtime overlay)scp’s$(AIE_PDI)→$(AIE_BOARD_IP):/boot/aie/<name>.pdiand thepartition.confsidecar →$(AIE_BOARD_IP):/boot/aie/<name>.partition.conf(warn-not-fail if the sidecar is absent — the PDI still uploads, butaie-partition-initwill not start for that image)Reboots the board (skippable with
-rfor stage-only uploads)Waits for ssh liveness, then verifies the Phase-2 startup-app-init boot loop loaded the AIE: the journal
AIE load: /boot/aie/<name>.pdiline, thefpga_managerdmesg record of writing<name>.pdi, an activeaie-partition-init@<name>.service, and the presence of/sys/class/aie
No device-tree overlay is deployed: the AIE PDI is a CDO-only partial PDI
delivered via /sys/class/fpga_manager/fpga0/firmware — the
ai_engine DT node is already live from the design’s pl.dtbo.
Projects that need richer verification (application-specific systemd
checks, xsdb readout, etc.) override the default by pointing
AIE_PROGRAM_SCRIPT at their own helper. The override must accept
the same flag contract:
Flag |
Meaning |
|---|---|
|
Runtime PDI to upload (required) |
|
Board target (required; forwarded from |
|
|
The default helper additionally accepts -r (stage-only: upload without
rebooting or verifying) and -h (show help); custom helpers may ignore
those if not relevant. The helper’s exit codes follow the convention
0 = success, 1 = local pre-flight failed, 2 = scp/ssh failure,
3 = post-reboot verification failed.
Key Variables
Variable |
Default |
Description |
|---|---|---|
|
|
Vitis workspace root. The AIE component lives at
|
|
|
Final deliverable directory (matches the HLS convention). |
|
|
Output dynamic PDI path written by |
|
|
Scratch directory used by the package step. |
|
|
Set to |
|
|
Log file path for the |
|
|
Deploy helper invoked by |
|
|
|
See Makefile Reference for the full AIE variable reference.
Troubleshooting
- “XILINX_VITIS not set”
The Makefile hard-fails at parse time if the Vitis environment was not sourced. Source the Vitis settings script (or a SLAC-local wrapper like
setup_env_slac.sh):source /path/to/Vitis/settings64.sh vitis --version # confirm 2025.1 or newer
- “Define either AIE_PLATFORM or AIE_PART”
Set exactly one.
AIE_PLATFORMfor dev boards with an AMD-shipped.xpfm;AIE_PARTfor custom Versal AIE boards without one.- “VIVADO_XSA_DIR not set” / “no .xsa found in …”
Only the
packagetarget requires the.xsa. SetVIVADO_XSA_DIRin the consuming Makefile to the upstream Vivado target’simages/directory and build that target’s.xsafirst — the newest.xsa(by the filename-encodedBUILD_TIMEtimestamp) is selected automatically.- “no <name>.partition.conf found” warning from ``make program``
The deploy helper could not find the
partition.confsidecar next to the PDI (and noAIE_CONFoverride was given). The PDI still uploads, butaie-partition-init@<name>.servicewill not start for that image. Runmake partition_conf(or a baremake) first so the sidecar lands inip/next to the PDI.- “Refusing to upload static PDI as runtime overlay”
The default deploy helper refuses any
-pargument containing_staticin the filename.<name>_static.pdiis theBOOT.BINhalf of a segmented-configuration build (see How to Use Versal Segmented Configuration); the runtime overlay is<name>.pdi(legacy<name>_aie_dynamic.pdi/<name>_dynamic.pdinames also work — the suffix is stripped on upload). Point-pat the dynamic half.- ``v++ –package`` fails on packaging
As a fallback path, set
USE_BOOTGEN_FALLBACK=1to package viabootgeninstead. The two paths produce a functionally equivalent dynamic PDI for the AIE-overlay case.- Subdirectory headers go missing from the component
AIE_SOURCESdirectory entries are non-recursive. If your graph references sources via a subdirectory path (e.g.#include "kernels/foo.h"oradf::source(k) = "kernels/foo.cc";), add the directory as a separate entry with a matching:dest_subdir(e.g.aie/kernels:kernels) so the files import into<component>/kernels/— the matchinginclude=path is auto-added to the generated cfg.