Skip to content
140 changes: 140 additions & 0 deletions test/npu-xrt/local_reset/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<!---//===- 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`, `dma`, and `switch` 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, re-push BD, re-arm lock |
| [`switch`](switch/README.md) | Stream-switch connection | `Stream_Switch_Slave_DMA_0_Config` (`0x3F104`) | re-enable slave port (torn down each dispatch end), re-arm lock |

The write *type* follows the driver per family: `core` and `dma` reset with masked
`maskwrite32` (aie-rt's `XAie_CoreReset`/`Unreset`/`Enable` and `XAie_DmaChannelReset` are `MaskWrite32`
of one bit-field), while `switch` and the lock re-arm use full-word `write32`
(aie-rt's `XAie_StrmConnCctEnable`/`XAie_LockSetValue` are `Write32`).

## 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. (There is no
stream-switch reset op, so `switch` has no op-based counterpart.)

| 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 re-push
BD + lock re-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 three 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 quiescent.** The reset is issued only when the target is settled --
a core halted at `aie.end`, or a run-forever channel stalled on a lock acquire.
The register 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) and
`aiex.npu.write32` (switch config, lock re-arm), plus `aiex.npu.push_queue` for
the 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 failure modes (no reset, reset without re-arm, disable
without re-enable) **hang by design**: a collect that never completes. They 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 three 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` |
| `Stream_Switch_Slave_DMA_0_Config` (bit 31 = `Slave_Enable`) | `0x3F104` | `0x3F104` |
| `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 three, 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), `aiex.npu.write32` where it is a `Write32` (switch
config, lock re-arm), plus `aiex.npu.push_queue` for the DMA start queue.

| 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` |
| Stream-switch slave port enable | `..._CORE_MODULE_STREAM_SWITCH_SLAVE_CONFIG_DMA_0` (`0x3F104`), `_SLAVE_ENABLE_LSB`=31 | `XAie_StrmConnCctEnable` / `XAie_StrmConnCctDisable` -- `driver/src/stream_switch/xaie_ss.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`); `XAie_StrmConnCctEnable`/`Disable` and `XAie_LockSetValue` are `Write32`, so
the `switch` slave-port toggle and the lock re-arm use full-word `write32`. (One
deviation: `XAie_StrmConnCctEnable`/`Disable` write both the master- and slave-port
config registers; the `switch` test writes only the slave register because the
master route is resident -- see [`switch/README.md`](switch/README.md#reference).)
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
Loading
Loading