Skip to content

Commit 53c1743

Browse files
authored
Merge branch 'main' into ehunhoff/issue-2566-dma-layout-checks
2 parents aeb7599 + b4f0388 commit 53c1743

68 files changed

Lines changed: 1245 additions & 712 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/lintAndFormat.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ jobs:
143143
pip install --upgrade pip
144144
pip install -r python/requirements_dev.txt
145145
146+
- name: Check filename case conflicts
147+
run: pre-commit run check-case-conflict --all-files --color never
148+
146149
- name: Run git-clang-format
147150
id: git-clang-format
148151
run: |

.github/workflows/scorecard.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,6 @@ jobs:
3939
retention-days: 5
4040

4141
- name: Upload to code-scanning
42-
uses: github/codeql-action/upload-sarif@7fd177fa680c9881b53cdab4d346d32574c9f7f4 # v3.35.4
42+
uses: github/codeql-action/upload-sarif@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1
4343
with:
4444
sarif_file: results.sarif

include/aie-c/TargetModel.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ MLIR_CAPI_EXPORTED int aieTargetModelRows(AieTargetModel targetModel);
5555
/// Returns true if this is an NPU target model.
5656
MLIR_CAPI_EXPORTED bool aieTargetModelIsNPU(AieTargetModel targetModel);
5757

58+
/// Returns the AIE architecture (as the underlying value of
59+
/// xilinx::AIE::AIEArch: AIE1=1, AIE2=2, AIE2p=3).
60+
MLIR_CAPI_EXPORTED uint32_t
61+
aieTargetModelGetTargetArch(AieTargetModel targetModel);
62+
5863
/// Returns the tile type for the given coordinates.
5964
MLIR_CAPI_EXPORTED uint32_t
6065
aieTargetModelGetTileType(AieTargetModel targetModel, int col, int row);

lib/CAPI/TargetModel.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,10 @@ bool aieTargetModelIsNPU(AieTargetModel targetModel) {
201201
return unwrap(targetModel).hasProperty(xilinx::AIE::AIETargetModel::IsNPU);
202202
}
203203

204+
uint32_t aieTargetModelGetTargetArch(AieTargetModel targetModel) {
205+
return static_cast<uint32_t>(unwrap(targetModel).getTargetArch());
206+
}
207+
204208
uint32_t aieTargetModelGetColumnShift(AieTargetModel targetModel) {
205209
return unwrap(targetModel).getColumnShift();
206210
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
##===- Makefile -----------------------------------------------------------===##
2+
#
3+
# This file licensed under the Apache License v2.0 with LLVM Exceptions.
4+
# See https://llvm.org/LICENSE.txt for license information.
5+
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
#
7+
# Copyright (C) 2026, Advanced Micro Devices, Inc.
8+
#
9+
##===----------------------------------------------------------------------===##
10+
11+
srcdir := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
12+
13+
include ${srcdir}/../../makefile-common
14+
15+
devicename ?= npu2
16+
targetname = custom_dma
17+
18+
.PHONY: all clean
19+
20+
all: build/final.xclbin
21+
22+
build/aie.mlir: ${srcdir}/${targetname}.py
23+
mkdir -p ${@D}
24+
python3 $< -d ${devicename} > $@
25+
26+
build/final.xclbin: build/aie.mlir
27+
mkdir -p ${@D}
28+
cd ${@D} && aiecc --aie-generate-xclbin --aie-generate-npu-insts \
29+
--no-xchesscc --no-xbridge \
30+
--xclbin-name=${@F} --npu-insts-name=insts.bin $(<:%=../%)
31+
32+
run_py: build/final.xclbin
33+
${powershell} python3 ${srcdir}/test.py -x build/final.xclbin -i build/insts.bin -k MLIR_AIE
34+
35+
clean:
36+
rm -rf build _build
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<!---//===- README.md -----------------------------------------*- Markdown -*-===//
2+
//
3+
// This file is licensed under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
// Copyright (C) 2026, Advanced Micro Devices, Inc.
8+
//
9+
//===----------------------------------------------------------------------===//-->
10+
11+
# Custom DMA
12+
13+
This IRON design demonstrates how to program custom DMA patterns that integrate with the IRON API by using a `Resolvable` subclass. `ScatterReadDMA` is automatically discovered in the Worker's `fn_args` by `Program`, which calls `resolve()` to emit the DMA configuration.
14+
15+
## Source Files Overview
16+
17+
1. `custom_dma.py`: A Python script that defines the IRON design. It contains a `ScatterReadDMA` class (a `Resolvable` subclass) that emits custom locks, DMA buffer descriptors, and flows alongside standard IRON components.
18+
19+
2. `test.py`: Host program to run the design on the NPU and verify the output against expected values.
20+
21+
## Design Overview
22+
23+
A 4×16 matrix of `int32` values is pre-loaded on a MemTile via `initial_value`. A custom three-BD DMA chain reads rows 0, 1, and 3 (skipping row 2) using per-BD offsets with non-uniform spacing. Each BD transfers 16 elements to a compute tile, which copies them to an output ObjectFifo that drains to the host.
24+
25+
```
26+
MemTile buffer (64 x i32):
27+
row 0 [0..15] = [100, 101, ..., 115] ← BD1 reads here
28+
row 1 [16..31] = [200, 201, ..., 215] ← BD2 reads here
29+
row 2 [32..47] = [300, 301, ..., 315] (skipped)
30+
row 3 [48..63] = [400, 401, ..., 415] ← BD3 reads here
31+
```
32+
The MemTile DMA is triggered from the runtime sequence via `set_lock_value`, which ensures the shim drain is configured before data starts flowing.
33+
34+
## Usage
35+
36+
### Compilation
37+
38+
To compile the design:
39+
40+
```shell
41+
make
42+
```
43+
44+
### Python Testbench
45+
46+
To run the design:
47+
48+
```shell
49+
make run_py
50+
```
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
# custom_dma/custom_dma.py -*- Python -*-
2+
#
3+
# This file is licensed under the Apache License v2.0 with LLVM Exceptions.
4+
# See https://llvm.org/LICENSE.txt for license information.
5+
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
#
7+
# (c) Copyright 2026 Advanced Micro Devices, Inc. or its affiliates
8+
9+
import numpy as np
10+
import argparse
11+
import sys
12+
13+
from aie.iron import ObjectFifo, Program, Runtime, Worker
14+
from aie.iron.controlflow import range_
15+
from aie.iron.device import NPU2, AnyComputeTile, AnyMemTile
16+
from aie.iron.resolvable import Resolvable
17+
from aie.dialects.aiex import set_lock_value
18+
19+
20+
class ScatterReadDMA(Resolvable):
21+
"""Read three equal-sized sections at non-uniform offsets from a MemTile buffer.
22+
23+
A custom three-BD chain pattern reads ``transfer_len`` elements from ``offset_a``,
24+
``offset_b``, and ``offset_c``, cycling.
25+
26+
Usage: pass an instance as a Worker ``fn_arg``. Inside the worker
27+
function, call ``acquire(1)`` / ``release(1)`` to synchronize with
28+
each BD completion.
29+
"""
30+
31+
def __init__(
32+
self,
33+
buf_type,
34+
initial_value: np.ndarray,
35+
recv_type,
36+
name: str,
37+
transfer_len: int,
38+
offset_a: int,
39+
offset_b: int,
40+
offset_c: int,
41+
memtile_placement=None,
42+
compute_placement=None,
43+
):
44+
self._buf_type = buf_type
45+
self._initial_value = np.asarray(initial_value, dtype=np.int32)
46+
self._recv_type = recv_type
47+
self._name = name
48+
self._transfer_len = transfer_len
49+
self._offset_a = offset_a
50+
self._offset_b = offset_b
51+
self._offset_c = offset_c
52+
self._memtile = memtile_placement
53+
self._compute = compute_placement
54+
55+
# Set by resolve(); used by acquire/release at kernel time.
56+
self._comp_cons_lock = None
57+
self._comp_prod_lock = None
58+
self._recv_buf = None
59+
60+
def tiles(self) -> list:
61+
ts = []
62+
if self._memtile is not None:
63+
ts.append(self._memtile)
64+
if self._compute is not None:
65+
ts.append(self._compute)
66+
return ts
67+
68+
def acquire(self, n: int = 1):
69+
from aie.dialects.aie import use_lock, LockAction
70+
71+
use_lock(self._comp_cons_lock, LockAction.AcquireGreaterEqual)
72+
return self._recv_buf
73+
74+
def release(self, n: int = 1):
75+
from aie.dialects.aie import use_lock, LockAction
76+
77+
use_lock(self._comp_prod_lock, LockAction.Release)
78+
79+
def resolve(self, loc=None, ip=None) -> None:
80+
from aie.dialects.aie import (
81+
buffer,
82+
lock,
83+
flow,
84+
memtile_dma,
85+
mem,
86+
dma_start,
87+
dma_bd,
88+
next_bd,
89+
use_lock,
90+
DMAChannelDir,
91+
LockAction,
92+
WireBundle,
93+
EndOp,
94+
)
95+
96+
memtile_op = self._memtile.op
97+
compute_op = self._compute.op
98+
99+
# --- MemTile side ---
100+
# cons_lock starts at 0 so the DMA is blocked until the runtime
101+
# sequence triggers it via set_lock_value.
102+
mem_prod_lock = lock(memtile_op, init=0)
103+
mem_cons_lock = lock(memtile_op, init=0)
104+
self._mem_cons_lock = mem_cons_lock
105+
106+
src_buf = buffer(
107+
memtile_op,
108+
self._buf_type,
109+
self._name,
110+
initial_value=self._initial_value,
111+
)
112+
113+
# --- Compute tile side ---
114+
comp_prod_lock = lock(compute_op, init=1)
115+
comp_cons_lock = lock(compute_op, init=0)
116+
117+
recv_buf = buffer(compute_op, self._recv_type, f"{self._name}_recv")
118+
119+
self._comp_cons_lock = comp_cons_lock
120+
self._comp_prod_lock = comp_prod_lock
121+
self._recv_buf = recv_buf
122+
123+
# --- DMA flow: MemTile MM2S → compute S2MM ---
124+
flow(memtile_op, WireBundle.DMA, 0, compute_op, WireBundle.DMA, 0)
125+
126+
# --- MemTile DMA: three-BD chain with non-uniform offsets ---
127+
@memtile_dma(memtile_op)
128+
def _mtdma(block):
129+
dma_start(DMAChannelDir.MM2S, 0, dest=block[1], chain=block[4])
130+
with block[1]: # BD1: row at offset_a
131+
use_lock(mem_cons_lock, LockAction.AcquireGreaterEqual)
132+
dma_bd(src_buf, offset=self._offset_a, len=self._transfer_len)
133+
use_lock(mem_prod_lock, LockAction.Release)
134+
next_bd(block[2])
135+
with block[2]: # BD2: row at offset_b
136+
use_lock(mem_cons_lock, LockAction.AcquireGreaterEqual)
137+
dma_bd(src_buf, offset=self._offset_b, len=self._transfer_len)
138+
use_lock(mem_prod_lock, LockAction.Release)
139+
next_bd(block[3])
140+
with block[3]: # BD3: row at offset_c
141+
use_lock(mem_cons_lock, LockAction.AcquireGreaterEqual)
142+
dma_bd(src_buf, offset=self._offset_c, len=self._transfer_len)
143+
use_lock(mem_prod_lock, LockAction.Release)
144+
next_bd(block[1])
145+
with block[4]:
146+
EndOp()
147+
148+
# --- Compute tile DMA: S2MM, loops forever ---
149+
@mem(compute_op)
150+
def _cdma(block):
151+
dma_start(DMAChannelDir.S2MM, 0, dest=block[1], chain=block[2])
152+
with block[1]:
153+
use_lock(comp_prod_lock, LockAction.AcquireGreaterEqual)
154+
dma_bd(recv_buf)
155+
use_lock(comp_cons_lock, LockAction.Release)
156+
next_bd(block[1])
157+
with block[2]:
158+
EndOp()
159+
160+
161+
def custom_dma_design(dev):
162+
# Buffer layout on MemTile: 4 rows × 16 columns (64 x i32).
163+
# Three BDs read rows 0, 1, and 3 — gaps of 1 and 2 rows (non-uniform).
164+
cols = 16
165+
total_elems = 4 * cols
166+
transfer_len = cols
167+
offset_a = 0 * cols # row 0
168+
offset_b = 1 * cols # row 1
169+
offset_c = 3 * cols # row 3 (skips row 2)
170+
171+
buf_type = np.ndarray[(total_elems,), np.dtype[np.int32]]
172+
transfer_type = np.ndarray[(transfer_len,), np.dtype[np.int32]]
173+
174+
init_data = np.zeros(total_elems, dtype=np.int32)
175+
for r in range(4):
176+
init_data[r * cols : (r + 1) * cols] = np.arange(
177+
(r + 1) * 100, (r + 1) * 100 + cols, dtype=np.int32
178+
)
179+
180+
# Request one MemTile and one compute tile; the placer assigns coordinates.
181+
memtile = AnyMemTile.copy()
182+
compute = AnyComputeTile.copy()
183+
184+
scatter = ScatterReadDMA(
185+
buf_type=buf_type,
186+
initial_value=init_data,
187+
recv_type=transfer_type,
188+
name="scatter_buf",
189+
transfer_len=transfer_len,
190+
offset_a=offset_a,
191+
offset_b=offset_b,
192+
offset_c=offset_c,
193+
memtile_placement=memtile,
194+
compute_placement=compute,
195+
)
196+
197+
of_out = ObjectFifo(transfer_type, depth=1, name="out")
198+
199+
def core_fn(scatter_h, of_out, n):
200+
# Row 0
201+
chunk = scatter_h.acquire(1)
202+
elem_out = of_out.acquire(1)
203+
for i in range_(n):
204+
elem_out[i] = chunk[i]
205+
scatter_h.release(1)
206+
of_out.release(1)
207+
# Row 1
208+
chunk = scatter_h.acquire(1)
209+
elem_out = of_out.acquire(1)
210+
for i in range_(n):
211+
elem_out[i] = chunk[i]
212+
scatter_h.release(1)
213+
of_out.release(1)
214+
# Row 3
215+
chunk = scatter_h.acquire(1)
216+
elem_out = of_out.acquire(1)
217+
for i in range_(n):
218+
elem_out[i] = chunk[i]
219+
scatter_h.release(1)
220+
of_out.release(1)
221+
222+
worker = Worker(
223+
core_fn,
224+
[scatter, of_out.prod(), transfer_len],
225+
tile=compute,
226+
while_true=True,
227+
)
228+
229+
out_type = np.ndarray[(transfer_len * 3,), np.dtype[np.int32]]
230+
231+
def rt_start_memtile_dma(scatter_obj):
232+
set_lock_value(scatter_obj._mem_cons_lock, 3)
233+
234+
rt = Runtime()
235+
with rt.sequence(out_type, out_type, out_type) as (_, b_out, _):
236+
rt.start(worker)
237+
tg = rt.task_group()
238+
rt.drain(of_out.cons(), b_out, wait=True, task_group=tg)
239+
rt.inline_ops(rt_start_memtile_dma, [scatter])
240+
rt.finish_task_group(tg)
241+
242+
return Program(dev, rt).resolve_program()
243+
244+
245+
p = argparse.ArgumentParser()
246+
p.add_argument("-d", "--dev", required=True, dest="device", help="AIE Device")
247+
opts = p.parse_args(sys.argv[1:])
248+
249+
if opts.device == "npu2":
250+
dev = NPU2()
251+
else:
252+
raise ValueError(f"[ERROR] Device name {opts.device} is unknown")
253+
254+
print(custom_dma_design(dev))
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// (c) Copyright 2026 Advanced Micro Devices, Inc.
2+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
3+
//
4+
// REQUIRES: ryzen_ai_npu2, peano
5+
//
6+
// RUN: mkdir -p test_stx
7+
// RUN: cd test_stx
8+
// RUN: make -f %S/Makefile clean
9+
// RUN: make -f %S/Makefile devicename=npu2
10+
// RUN: %run_on_npu2% make -f %S/Makefile run_py devicename=npu2

0 commit comments

Comments
 (0)