Skip to content
137 changes: 137 additions & 0 deletions test/npu-xrt/local_reset/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<!---//===- README.md ---------------------------------------*- Markdown -*-===//
//
// Copyright (C) 2026 Advanced Micro Devices, Inc.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//-->

# Local reset

On-board tests that exercise **local reset**: returning a single tile-local
hardware block to a clean state from the runtime sequence, without rebuilding the
design or reloading the array. Each family resets one resettable block and shows
that the design keeps working across it. The `core` and `dma` families issue the
reset as **raw register writes**; the two `*_op` families issue it through the
merged `aiex.core_reset` / `aiex.dma_channel_reset` runtime-sequence ops, which
drive the same registers (see [Op-based variants](#op-based-variants)).

| Family | Block reset | Key register(s) | Reset mechanism |
|--------|-------------|-----------------|-----------------|
| [`core`](core/README.md) | AI Engine core | `Core_Control` (`0x32000`) | masked reset -> unreset -> enable (one field per write) |
| [`dma`](dma/README.md) | MM2S DMA channel | `DMA_MM2S_0_Ctrl` (`0x1DE10`) | masked reset pulse flushes a queued BD, then push good BD + arm lock |

Both families reset with the driver's write *type*: masked `maskwrite32` (aie-rt's
`XAie_CoreReset`/`Unreset`/`Enable` and `XAie_DmaChannelReset` are `MaskWrite32` of
one bit-field). The `dma` lock re-arm uses the `aiex.set_lock` op, which lowers to
the full-word write of the lock value register that aie-rt's `XAie_LockSetValue`
performs.

## Op-based variants

The two `*_op` families are the on-board counterparts of `core` and `dma` that
drive the merged reset ops
([#3375](https://github.com/Xilinx/mlir-aie/pull/3375) /
[#3370](https://github.com/Xilinx/mlir-aie/pull/3370)) rather than issuing the
`maskwrite32` reset pulse directly, as the raw families do.

| Family | Op | Lowers to | Raw-register sibling |
|--------|----|-----------|----------------------|
| [`core_reset_op`](core_reset_op/README.md) | `aiex.core_reset` | mask-preserving reset pulse on `Core_Control` (`0x32000`, bit 1) | [`core`](core/README.md) |
| [`dma_channel_reset_op`](dma_channel_reset_op/README.md) | `aiex.dma_channel_reset` | mask-preserving reset pulse on `DMA_MM2S_0_Ctrl` (`0x1DE10`, bit 1) | [`dma`](dma/README.md) |

Both ops lower (in the default `aiecc` pipeline) to a two-write
`aiex.npu.maskwrite32` pulse on the **same register and reset bit** the raw
sibling writes, so the op-based and raw families exercise the same protocol. The
ops are **reset-only**: they mask to the reset bit (preserving the surrounding
fields) and do not re-enable, re-push, or re-arm. So:

- `dma_channel_reset_op` is a drop-in for `dma`'s masked reset pulse; the BD
enqueues + lock arm remain around it, and it passes unchanged.
- `core_reset_op` supplies the `reset -> unreset` pulse
(`XAie_CoreReset`/`XAie_CoreUnreset`); because this core has run to `aie.end` (no
longer enabled) it composes the op with a **masked** re-enable mirroring
`XAie_CoreEnable`, so the whole test is the driver's
`XAie_CoreReset -> XAie_CoreUnreset -> XAie_CoreEnable` sequence, every write
masked to one field. The op *alone* does not re-enable, so on its own it leaves
such a core halted; this pins the op's documented scope (it assumes a
still-enabled resident core). See its README.

## Shared design

All families run on a single column, core tile `(0,2)` -> shim `(0,0)`:

- **Resident workload.** The data stays in tile memory across dispatches, so a
correct reset means every dispatch returns identical bytes (or, for `core`, a
deterministic `+1` from a counter kept in data memory).
- **Reset while settled.** The reset acts on a settled block -- a core halted at
`aie.end`, or a DMA channel stalled on a lock acquire with a BD queued behind the
lock. The writes take effect on a settled block; they are not used to preempt a
running one.
- **Driven from the runtime sequence.** Each test issues the reset from
`aie.runtime_sequence` with `aiex.npu.maskwrite32` (core/dma reset), plus
`aiex.set_lock` (`dma` lock re-arm) and `aiex.npu.push_queue` (`dma` start
queue), then dispatches and checks the host-visible result.

## What the automated run covers

`run.lit` runs **only the correct protocol** -- the reset sequence that keeps the
design working. The `dma` families are falsifiable: removing the reset leaves a
"bad" buffer descriptor queued ahead of the good one, so the host collects the
wrong bytes -- a clean failure rather than a hang. The remaining negatives (reset
without re-push/re-arm) **hang by design**: a collect that never completes. All are
described in each family's README and reproduced by hand by editing the reset
sequence in `aie.mlir`; they are not part of the automated run.

## Architecture and register offsets

The tests write raw tile-local offsets. All families target both NPU generations
-- **npu1** (Phoenix, AIE-ML) and **npu2** (Strix, AIE2P) -- because every offset
and bit field they touch is the same on both:

| Register | npu1 (AIE-ML) | npu2 (AIE2P) |
|----------|---------------|--------------|
| `Core_Control` | `0x32000` | `0x32000` |
| `DMA_MM2S_0_Ctrl` / `_Start_Queue` | `0x1DE10` / `0x1DE14` | `0x1DE10` / `0x1DE14` |
| `LOCK0_VALUE` | `0x1F000` | `0x1F000` |

In aie-rt the raw offsets are the `XAIEMLGBL_*` defines (`xaiemlgbl_params.h`,
used for npu1 via `XAIE_DEV_GEN_AIE2IPU`) and the matching `XAIE2PGBL_*` defines
(`xaie2pgbl_params.h`, npu2); see [References](#references). (The older AIE1 /
Versal parts in `xaiegbl_params.h` use a different layout, but they are not NPU
targets and are not exercised here.)

## Running

```
lit -sv core/run.lit # one family
lit -sv build/test/npu-xrt/local_reset # all families, from the build dir
```

Requires a Ryzen AI device (`REQUIRES: ryzen_ai`). The board generation is
auto-detected; every family runs on both npu1 (Phoenix) and npu2 (Strix).

## References

Every offset, bit field, and reset procedure asserted here is defined in the
vendored AI Engine driver **aie-rt** (public: <https://github.com/Xilinx/aie-rt>),
under `third_party/aie-rt/` in this repo. The table lists the AIE2P (npu2) names
from `driver/src/global/xaie2pgbl_params.h`; the npu1 (AIE-ML) equivalents are the
identically-suffixed `XAIEMLGBL_*` defines in `xaiemlgbl_params.h` at the same
offsets. The tests issue each protocol directly, with the **same register-write
type the driver routine uses** -- `aiex.npu.maskwrite32` where the driver is a
`MaskWrite32` (core/dma reset), plus `aiex.npu.push_queue` for the DMA start
queue. The lock re-arm uses the `aiex.set_lock` op, which lowers to the `Write32`
of the lock value register that `XAie_LockSetValue` performs.

| Protocol (test) | Register define -- `xaie2pgbl_params.h` / `xaiemlgbl_params.h` | Driver routine |
|-----------------|----------------------------------------|----------------|
| Core reset -> unreset -> enable | `..._CORE_MODULE_CORE_CONTROL` (`0x32000`), `_RESET_LSB`=1, `_ENABLE_LSB`=0 | `XAie_CoreReset` / `XAie_CoreUnreset` / `XAie_CoreEnable` -- `driver/src/core/xaie_core.c` |
| DMA channel reset | `..._MEMORY_MODULE_DMA_MM2S_0_CTRL` (`0x1DE10`), `_RESET_LSB`=1, `_ENABLE_LSB`=0 | `XAie_DmaChannelReset` -- `driver/src/dma/xaie_dma.c` |
| Re-push BD to start queue | `..._MEMORY_MODULE_DMA_MM2S_0_START_QUEUE` (`0x1DE14`) | `XAie_DmaChannelPushBdToQueue` -- `driver/src/dma/xaie_dma.c` |
| Lock re-arm (set value) | `..._MEMORY_MODULE_LOCK0_VALUE` (`0x1F000`) | `XAie_LockSetValue` -- `driver/src/locks/xaie_locks.c` |

Each test issues these as the driver's own write type: `XAie_CoreReset`/`Unreset`/
`Enable` and `XAie_DmaChannelReset` are `MaskWrite32` of a single bit-field, so the
`core` and `dma` tests use `maskwrite32` (reset/unreset mask `0x2`, enable mask
`0x1`); the `dma` lock re-arm uses the `aiex.set_lock` op, which lowers to the
full-word `write32` that `XAie_LockSetValue` performs.
48 changes: 48 additions & 0 deletions test/npu-xrt/local_reset/core/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!---//===- README.md ---------------------------------------*- Markdown -*-===//
//
// Copyright (C) 2026 Advanced Micro Devices, Inc.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//-->

# Core reset

Restarting a finite core kernel from a clean program counter by driving its
`Core_Control` register (tile-local offset `0x32000`) with masked `maskwrite32`s,
one per field, exactly as the aie-rt driver does. Bit 0 is `Enable`, bit 1 is
`Reset`; the offset and bit layout are identical on AIE-ML (npu1) and AIE2P (npu2),
so this family runs on both. See [`../README.md`](../README.md) for the shared
design and how to run.

The core on tile `(0, 2)` runs once, fills the output buffer with a data-memory
counter, increments the counter, then halts at `aie.end`. Data memory survives a
reset, but the program counter does not -- so a restart re-runs the kernel and
emits `counter + 1`. The host collects two batches (before/after the restart) and
checks `batch2 == batch1 + 1`.

The reset issues `reset -> unreset -> enable` after batch 1, each a masked write of
a single `Core_Control` field (mirroring `XAie_CoreReset` / `XAie_CoreUnreset` /
`XAie_CoreEnable`). These writes only take effect on a **settled** core (the kernel
has reached `aie.end`); they are issued between two completed transfers, not to
preempt a running core.

## Behaviour

- **Correct protocol (this test):** `reset -> unreset -> enable` re-runs the core ->
`batch2 == batch1 + 1`.
- **No reset / reset without enable:** the core stays `Done`/halted, so the second
batch never arrives and the collect never completes. `Enable` (bit 0) is what
re-runs a settled core; the reset bit clears state but does not restart on its
own. This mode hangs by design -- reproduce it by hand by editing the reset
sequence in `aie.mlir`.

## Reference

`Core_Control` and its `Reset`/`Enable` bits, and the reset/unreset/enable
procedure, are defined in the public aie-rt driver
(<https://github.com/Xilinx/aie-rt>, vendored at `third_party/aie-rt/`):
`XAIE2PGBL_CORE_MODULE_CORE_CONTROL` in `driver/src/global/xaie2pgbl_params.h`,
and `XAie_CoreReset` / `XAie_CoreUnreset` / `XAie_CoreEnable` in
`driver/src/core/xaie_core.c` (each a `MaskWrite32` of a single `Core_Control`
field, as this test issues them). See [`../README.md`](../README.md#references) for
the full table.
81 changes: 81 additions & 0 deletions test/npu-xrt/local_reset/core/aie.mlir
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//===- aie.mlir ------------------------------------------------*- MLIR -*-===//
//
// Copyright (C) 2026 Advanced Micro Devices, Inc.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

// Core reset: restart a halted core via CORE_CONTROL raw write32s. See README.md.

module {
aie.device(NPUDEVICE) {
%tile_0_0 = aie.tile(0, 0)
%tile_0_2 = aie.tile(0, 2)

%prod_lock = aie.lock(%tile_0_2, 0) {init = 1 : i32, sym_name = "prod_lock"}
%cons_lock = aie.lock(%tile_0_2, 1) {init = 0 : i32, sym_name = "cons_lock"}

%counter = aie.buffer(%tile_0_2) {sym_name = "counter"} : memref<1xi32>
%output_buffer = aie.buffer(%tile_0_2) {sym_name = "output_buffer"} : memref<8xi32>

aie.flow(%tile_0_2, DMA : 0, %tile_0_0, DMA : 0)

%core_0_2 = aie.core(%tile_0_2) {
%c0 = arith.constant 0 : index
%c1 = arith.constant 1 : index
%c8 = arith.constant 8 : index
%c1_i32 = arith.constant 1 : i32

aie.use_lock(%prod_lock, AcquireGreaterEqual, %c1_i32)
%n = memref.load %counter[%c0] : memref<1xi32>
scf.for %i = %c0 to %c8 step %c1 {
memref.store %n, %output_buffer[%i] : memref<8xi32>
}
%n1 = arith.addi %n, %c1_i32 : i32
memref.store %n1, %counter[%c0] : memref<1xi32>
aie.use_lock(%cons_lock, Release, %c1_i32)
aie.end
}

%mem_0_2 = aie.mem(%tile_0_2) {
%0 = aie.dma_start(MM2S, 0, ^bb1, ^bb2)
^bb1:
%c1_lk = arith.constant 1 : i32
aie.use_lock(%cons_lock, AcquireGreaterEqual, %c1_lk)
aie.dma_bd(%output_buffer : memref<8xi32>) { len = 8 : i32 }
aie.use_lock(%prod_lock, Release, %c1_lk)
aie.next_bd ^bb1
^bb2:
aie.end
}

aie.shim_dma_allocation @out0 (%tile_0_0, S2MM, 0)

aie.runtime_sequence @seq(%arg0: memref<16xi32>) {
// Batch 1: the core ran once from load; collect counter=N into arg0[0:8].
aiex.npu.dma_memcpy_nd(%arg0[0, 0, 0, 0][1, 1, 1, 8][0, 0, 0, 1]) {id = 0 : i64, issue_token = true, metadata = @out0} : memref<16xi32>
aiex.npu.dma_wait {symbol = @out0}

// Core reset protocol on tile (0,2) CORE_CONTROL (0x32000 = 204800), driving
// each field with a masked write exactly as the aie-rt driver does:
// XAie_CoreReset : reset bit 1 -> 1 (mask 0x2)
// XAie_CoreUnreset : reset bit 1 -> 0 (mask 0x2)
// XAie_CoreEnable : ENABLE bit 0 -> 1 (mask 0x1)
// Each maskwrite32 touches one field, leaving the rest of CORE_CONTROL intact
// (all three driver routines are MaskWrite32 of a single bit-field).
%cc = arith.constant 204800 : i32
%reset = arith.constant 2 : i32
%unreset = arith.constant 0 : i32
%enable = arith.constant 1 : i32
%rst_mask = arith.constant 2 : i32
%en_mask = arith.constant 1 : i32
aiex.npu.maskwrite32(%cc, %reset, %rst_mask) {column = 0 : i32, row = 2 : i32} : i32, i32, i32
aiex.npu.maskwrite32(%cc, %unreset, %rst_mask) {column = 0 : i32, row = 2 : i32} : i32, i32, i32
aiex.npu.maskwrite32(%cc, %enable, %en_mask) {column = 0 : i32, row = 2 : i32} : i32, i32, i32

// Batch 2: the core re-ran from a clean PC and emitted counter=N+1.
aiex.npu.dma_memcpy_nd(%arg0[0, 0, 0, 8][1, 1, 1, 8][0, 0, 0, 1]) {id = 1 : i64, issue_token = true, metadata = @out0} : memref<16xi32>
aiex.npu.dma_wait {symbol = @out0}
}
}
}
14 changes: 14 additions & 0 deletions test/npu-xrt/local_reset/core/run.lit
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (C) 2026 Advanced Micro Devices, Inc.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
// Core reset: reset -> unreset -> enable re-runs the core (see README.md).
//
// REQUIRES: ryzen_ai
//
// RUN: cp %S/aie.mlir aie_arch.mlir
// RUN: %run_on_npu1% sed 's/NPUDEVICE/npu1_1col/g' -i aie_arch.mlir
// RUN: %run_on_npu2% sed 's/NPUDEVICE/npu2_1col/g' -i aie_arch.mlir
// RUN: %aiecc %backend_flags --get-xclbin --get-npu-insts --alloc-scheme=basic-sequential --xclbin-name=aie.xclbin --npu-insts-name=insts.bin ./aie_arch.mlir
// RUN: %host_clang %S/test.cpp -o test.exe -std=c++17 -Wall %xrt_flags %host_link_flags %test_utils_flags
// RUN: %run_on_npu1% ./test.exe -x aie.xclbin -k MLIR_AIE -i insts.bin
// RUN: %run_on_npu2% ./test.exe -x aie.xclbin -k MLIR_AIE -i insts.bin
112 changes: 112 additions & 0 deletions test/npu-xrt/local_reset/core/test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//===- test.cpp -------------------------------------------------*- C++ -*-===//
//
// Copyright (C) 2026 Advanced Micro Devices, Inc.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

// Host side of the core-reset reproducer. The single runtime sequence collects
// two output batches into one 16-element buffer: arg0[0:8] before the reset
// pulse and arg0[8:16] after it. The core writes a data-memory counter into the
// buffer on each run and increments it, so:
// batch1 = N (the value at the core's first run)
// batch2 = N+1 (only produced if the reset restarted the core's PC)
// The counter's absolute start value is unspecified, so the proof the core
// re-ran is batch2 == batch1 + 1.

#include <cstdint>
#include <iostream>
#include <vector>

#include "cxxopts.hpp"
#include "test_utils.h"
#include "xrt/xrt_bo.h"
#include "xrt/xrt_device.h"
#include "xrt/xrt_kernel.h"

constexpr int BATCH = 8;
constexpr int OUT_SIZE = 2 * BATCH; // batch1 || batch2

int main(int argc, const char *argv[]) {
cxxopts::Options options("local_reset_core");
test_utils::add_default_options(options);

cxxopts::ParseResult vm;
test_utils::parse_options(argc, argv, options, vm);

std::vector<uint32_t> instr_v =
test_utils::load_instr_binary(vm["instr"].as<std::string>());

int verbosity = vm["verbosity"].as<int>();
if (verbosity >= 1)
std::cout << "Sequence instr count: " << instr_v.size() << "\n";

// Get a device handle and load the xclbin.
unsigned int device_index = 0;
auto device = xrt::device(device_index);
auto xclbin = xrt::xclbin(vm["xclbin"].as<std::string>());
std::string kernelName = vm["kernel"].as<std::string>();

device.register_xclbin(xclbin);
xrt::hw_context context(device, xclbin.get_uuid());
auto kernel = xrt::kernel(context, kernelName);

auto bo_instr = xrt::bo(device, instr_v.size() * sizeof(int),
XCL_BO_FLAGS_CACHEABLE, kernel.group_id(1));
auto bo_out = xrt::bo(device, OUT_SIZE * sizeof(int32_t),
XRT_BO_FLAGS_HOST_ONLY, kernel.group_id(3));

void *bufInstr = bo_instr.map<void *>();
memcpy(bufInstr, instr_v.data(), instr_v.size() * sizeof(int));
bo_instr.sync(XCL_BO_SYNC_BO_TO_DEVICE);

if (verbosity >= 1)
std::cout << "Running kernel.\n";
unsigned int opcode = 3;
auto run = kernel(opcode, bo_instr, instr_v.size(), bo_out);
ert_cmd_state r = run.wait();
if (r != ERT_CMD_STATE_COMPLETED) {
std::cout << "Kernel did not complete. Returned status: " << r << "\n";
return 1;
}

bo_out.sync(XCL_BO_SYNC_BO_FROM_DEVICE);
uint32_t *bufOut = bo_out.map<uint32_t *>();

int errors = 0;

// Each batch must be internally uniform (the core fills the whole buffer with
// one counter value).
uint32_t b1 = bufOut[0];
uint32_t b2 = bufOut[BATCH];
for (int i = 0; i < BATCH; i++) {
if (bufOut[i] != b1) {
std::cout << "batch1[" << i << "] = " << bufOut[i] << " != " << b1
<< "\n";
errors++;
}
if (bufOut[BATCH + i] != b2) {
std::cout << "batch2[" << i << "] = " << bufOut[BATCH + i] << " != " << b2
<< "\n";
errors++;
}
}

std::cout << "batch1 = " << b1 << ", batch2 = " << b2 << "\n";

// The proof the core restarted from a clean PC: it ran a second time and
// emitted the incremented counter.
if (b2 != b1 + 1) {
std::cout
<< "Core did not re-run after reset: expected batch2 == batch1 + 1"
<< "\n";
errors++;
}

if (!errors) {
std::cout << "\nPASS!\n\n";
return 0;
}
std::cout << "\nfailed.\n\n";
return 1;
}
Loading
Loading