Skip to content

Commit 03296b5

Browse files
hunhoffeclaude
andcommitted
[dyn-seq P1.4] Static-vs-dynamic TXN equivalence harness
Prove milestone #3222's correctness spine: a runtime_sequence compiled to a C++ TXN builder (aie-translate --aie-npu-to-cpp) produces a byte-identical TXN word stream to the production binary emitter (--aie-npu-to-binary) when invoked with matching constants, and runtime scalar args flow in as function parameters. Each test lowers a static and a dynamic sequence to terminal npu ops and does a three-way byte-identical compare: binary golden vs generate_txn_<static>() vs generate_txn_<dynamic>(N). The split localizes drift to EmitC codegen vs runtime-arg substitution. Coverage: - Both DMA lowering paths, which converge on the same terminal ops but nothing else in-tree exercises end to end: dma_memcpy_nd (memcpy_nd*.mlir) and the DMA-task path (dma_task*.mlir; dma_configure_task_for/start/await, SSA bd_id from #3225, the only path reaching the sync op). - Both device generations (npu2 + npu1 variants), since device info is baked into the header words. - Every op the C++ TXN target supports: write32, maskwrite32, blockwrite, address_patch, sync. - Two sizes on the memcpy path (N=4096 and N=8192) to exercise the add-a-size recipe and guard the runtime-arg path at more than one value. The DMA-task sequences carry aie.dma_bd in the declarative sizes/strides/offset/ len form (#3306); the C++ builder name is generate_txn_<device>_<seq>, so the comparator is parameterized against generate_txn_main_<seq>. compare_main.cpp is fully -D parameterized so one comparator serves every path, device, and size; README documents the add-a-size/add-a-pattern recipe. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 868d7cf commit 03296b5

6 files changed

Lines changed: 520 additions & 0 deletions

File tree

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
//===- compare_main.cpp ----------------------------------------*- C++ -*-===//
2+
//
3+
// Copyright (C) 2026 Advanced Micro Devices, Inc.
4+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5+
//
6+
//===----------------------------------------------------------------------===//
7+
//
8+
// Three-way TXN equivalence harness for the static-vs-dynamic tests.
9+
//
10+
// It proves the milestone #3222 correctness spine for one runtime sequence:
11+
//
12+
// 1. golden = the production binary emitter (aie-translate --aie-npu-to-
13+
// binary), read from the hex file passed as argv[1].
14+
// 2. STATIC_FN = the C++ TXN builder generated from the *static* sequence
15+
// (all-constant fields).
16+
// 3. DYN_FN(ARGVAL) = the C++ TXN builder generated from the *dynamic*
17+
// sequence, invoked with the same constant the static side
18+
// bakes in.
19+
//
20+
// All three word streams must be byte-identical. Splitting the check three
21+
// ways localizes a regression: golden-vs-STATIC isolates the EmitC codegen
22+
// against the binary emitter; STATIC-vs-DYN isolates runtime-argument
23+
// substitution.
24+
//
25+
// The generated header (which defines STATIC_FN and DYN_FN) is #included via
26+
// -DGEN_HDR; STATIC_FN / DYN_FN / ARGVAL come from -D so this one file serves
27+
// both DMA paths and any future size.
28+
//
29+
//===----------------------------------------------------------------------===//
30+
31+
#include GEN_HDR
32+
33+
#include <cstdint>
34+
#include <cstdio>
35+
#include <cstdlib>
36+
#include <fstream>
37+
#include <string>
38+
#include <vector>
39+
40+
namespace {
41+
42+
// Read a hex-per-line word stream (the "%08X\n" format emitted by
43+
// aie-translate --aie-npu-to-binary -aie-output-binary=false).
44+
std::vector<uint32_t> readHex(const char *path) {
45+
std::ifstream in(path);
46+
if (!in) {
47+
std::fprintf(stderr, "cannot open golden hex file '%s'\n", path);
48+
std::exit(2);
49+
}
50+
std::vector<uint32_t> words;
51+
std::string line;
52+
while (std::getline(in, line)) {
53+
if (line.empty())
54+
continue;
55+
words.push_back(
56+
static_cast<uint32_t>(std::strtoul(line.c_str(), nullptr, 16)));
57+
}
58+
return words;
59+
}
60+
61+
// Report the first divergence (or a length mismatch) and return false.
62+
bool equal(const char *aName, const std::vector<uint32_t> &a, const char *bName,
63+
const std::vector<uint32_t> &b) {
64+
size_t lim = a.size() < b.size() ? a.size() : b.size();
65+
for (size_t i = 0; i < lim; ++i) {
66+
if (a[i] != b[i]) {
67+
std::fprintf(stderr, "%s vs %s differ at word %zu: 0x%08x vs 0x%08x\n",
68+
aName, bName, i, a[i], b[i]);
69+
return false;
70+
}
71+
}
72+
if (a.size() != b.size()) {
73+
std::fprintf(stderr,
74+
"%s (%zu words) and %s (%zu words) have equal prefix "
75+
"but different length\n",
76+
aName, a.size(), bName, b.size());
77+
return false;
78+
}
79+
return true;
80+
}
81+
82+
} // namespace
83+
84+
int main(int argc, char **argv) {
85+
if (argc != 2) {
86+
std::fprintf(stderr, "usage: %s <golden.hex>\n", argv[0]);
87+
return 2;
88+
}
89+
90+
std::vector<uint32_t> golden = readHex(argv[1]);
91+
std::vector<uint32_t> stat = STATIC_FN();
92+
std::vector<uint32_t> dyn = DYN_FN(ARGVAL);
93+
94+
bool ok = equal("golden", golden, "static-C++", stat) &&
95+
equal("static-C++", stat, "dynamic-C++", dyn);
96+
97+
if (!ok)
98+
return 1;
99+
100+
std::printf("equivalent: %zu words (arg=%d)\n", golden.size(), (int)(ARGVAL));
101+
return 0;
102+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<!--
2+
Copyright (C) 2026 Advanced Micro Devices, Inc.
3+
SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
4+
-->
5+
6+
# Static-vs-dynamic TXN equivalence harness
7+
8+
These tests prove milestone #3222's correctness spine: a compiled-once
9+
`aie.runtime_sequence` produces the *same* TXN word stream whether its scalar
10+
fields are baked-in constants or supplied as runtime arguments.
11+
12+
Each test lowers two sequences — one static (all constants), one dynamic (takes
13+
an `i32` argument) — to terminal npu ops, then compares three word streams that
14+
must be byte-identical:
15+
16+
1. **golden** — the production binary emitter,
17+
`aie-translate --aie-npu-to-binary -aie-output-binary=false` on the static
18+
sequence.
19+
2. **`generate_txn_<static>()`** — the C++ TXN builder from
20+
`aie-translate --aie-npu-to-cpp`, same constants. Catches EmitC-codegen drift
21+
against the binary emitter.
22+
3. **`generate_txn_<dynamic>(N)`** — the C++ builder from the dynamic sequence,
23+
invoked with the constant the static side bakes in. Catches runtime-argument
24+
substitution drift.
25+
26+
`Inputs/compare_main.cpp` reads the golden hex file and calls the two generated
27+
functions (selected via `-DSTATIC_FN` / `-DDYN_FN`, argument via `-DARGVAL`,
28+
header via `-DGEN_HDR`), reporting the first divergent word on mismatch.
29+
30+
Two DMA lowering paths are covered because they converge on the same terminal
31+
ops but reach them differently:
32+
33+
- `memcpy_nd.mlir` — the `aiex.npu.dma_memcpy_nd` path (`--aie-dma-to-npu`).
34+
- `dma_task.mlir` — the DMA-task path
35+
(`dma_configure_task_for` / `dma_start_task` / `dma_await_task`, carrying the
36+
SSA `bd_id`), which needs `--aie-substitute-shim-dma-allocations
37+
--aie-assign-runtime-sequence-bd-ids --aie-dma-tasks-to-npu` before the shared
38+
`--aie-dma-to-npu` step, and is the only path that exercises the `sync` op.
39+
40+
Both paths are covered on both device generations: `memcpy_nd.mlir` /
41+
`dma_task.mlir` target npu2, and `memcpy_nd_npu1.mlir` / `dma_task_npu1.mlir`
42+
target npu1. Device info is baked into the TXN header words, so equivalence is
43+
checked per generation.
44+
45+
Between them the tests exercise every op the C++ TXN target supports: `write32`,
46+
`maskwrite32`, `blockwrite`, `address_patch`, and `sync`.
47+
48+
Only `rtp_write` carries the runtime value: it is the most a runtime argument
49+
can drive without pushing the BD onto the per-register `write32` path, which
50+
would make the static and dynamic streams differ structurally rather than in
51+
value. Genuinely runtime-valued DMA sizes/strides arrive with the Phase-2
52+
dynamic BD-word encoder, which will extend this harness.
53+
54+
## Adding a new size
55+
56+
Add a static sequence that bakes the new value (e.g. `@memcpy_static_8192`, as
57+
in `memcpy_nd.mlir`), then add two RUN lines: one that emits its golden with
58+
`-aie-sequence-name=<that sequence>`, and one that compiles the comparator with
59+
`-DSTATIC_FN=<that sequence>` and `-DDYN_FN` still the shared dynamic sequence,
60+
passing the new value as `-DARGVAL=`. The dynamic sequence is reused unchanged.
61+
No C++ changes are needed — `compare_main.cpp` is fully parameterized by `-D`
62+
macros. (The static side needs its own sequence because the golden is the binary
63+
emitter run on all-constant fields.)
64+
65+
## Adding a new DMA pattern
66+
67+
Add another `aiex.npu.dma_memcpy_nd` (or another BD in a `dma_configure_task_for`)
68+
to *both* the static and dynamic sequences, keeping them structurally identical
69+
apart from the runtime argument. Remember the `aie.dma_bd` length is the product
70+
of the lowest three dimension sizes — the highest dimension is the BD repeat
71+
count, not part of the transfer length.
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// Copyright (C) 2026 Advanced Micro Devices, Inc.
4+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5+
//
6+
//===----------------------------------------------------------------------===//
7+
//
8+
// Static-vs-dynamic TXN equivalence for the DMA-task path
9+
// (dma_configure_task_for / dma_start_task / dma_await_task, carrying the SSA
10+
// bd_id introduced in #3225).
11+
//
12+
// This path lowers differently from dma_memcpy_nd: dma-tasks-to-npu first
13+
// produces npu.push_queue / npu.write_bd (and an npu.sync per awaited token),
14+
// which dma-to-npu then lowers to the same terminal write32 / blockwrite /
15+
// sync / address_patch ops. This test proves that convergence end to end -
16+
// nothing else in the tree exercises a DMA-task sequence all the way to a TXN
17+
// stream - and that the C++ builder is byte-identical to the binary emitter
18+
// on it, for both a static and a runtime-arg sequence.
19+
//
20+
// The await steps mean issue_token=true is required on the configures, so this
21+
// is also the path that exercises the sync op in the EmitC target. Two BD
22+
// shapes (repeat count in the highest dim; length is the product of the lower
23+
// three) cover more than one BD encoding.
24+
//
25+
// To add another size, see README.md in this directory.
26+
//
27+
//===----------------------------------------------------------------------===//
28+
29+
// REQUIRES: peano
30+
31+
// RUN: rm -rf %t.d && mkdir -p %t.d
32+
33+
// Lower the DMA-task ops to terminal npu ops. Unlike the dma_memcpy_nd path
34+
// this needs the shim substitution + BD-id assignment + dma-tasks-to-npu
35+
// stages before the shared dma-to-npu step.
36+
// RUN: aie-opt --aie-substitute-shim-dma-allocations \
37+
// RUN: --aie-assign-runtime-sequence-bd-ids --aie-dma-tasks-to-npu \
38+
// RUN: --aie-dma-to-npu %s -o %t.d/lowered.mlir
39+
40+
// Golden word stream from the production binary emitter.
41+
// RUN: aie-translate --aie-npu-to-binary -aie-output-binary=false \
42+
// RUN: -aie-sequence-name=task_static %t.d/lowered.mlir > %t.d/golden.hex
43+
44+
// One generated header holds both generate_txn_main_task_static/_dynamic.
45+
// RUN: aie-translate --aie-npu-to-cpp %t.d/lowered.mlir > %t.d/gen.h
46+
47+
// Host-compile and run the three-way comparator.
48+
// RUN: %host_clang -std=c++17 -I%S/../../../../include \
49+
// RUN: -DGEN_HDR='"%t.d/gen.h"' \
50+
// RUN: -DSTATIC_FN=generate_txn_main_task_static \
51+
// RUN: -DDYN_FN=generate_txn_main_task_dynamic -DARGVAL=4096 \
52+
// RUN: %S/Inputs/compare_main.cpp %host_link_flags -o %t.d/cmp.exe
53+
// RUN: %t.d/cmp.exe %t.d/golden.hex
54+
55+
module {
56+
aie.device(npu2) {
57+
%tile_0_0 = aie.tile(0, 0)
58+
%tile_0_2 = aie.tile(0, 2)
59+
%rtp = aie.buffer(%tile_0_2) {sym_name = "rtp", address = 49152 : i32} : memref<16xi32>
60+
aie.shim_dma_allocation @of_in (%tile_0_0, MM2S, 0)
61+
aie.shim_dma_allocation @of_out (%tile_0_0, S2MM, 0)
62+
63+
aie.runtime_sequence @task_static(%in: memref<8192xi32>, %out: memref<8192xi32>) {
64+
%c4096 = arith.constant 4096 : i32
65+
%c4097 = arith.constant 4097 : i32
66+
aiex.npu.rtp_write(@rtp, 0, %c4096) : i32
67+
aiex.npu.rtp_write(@rtp, 4, %c4097) : i32
68+
%tout = aiex.dma_configure_task_for @of_out {
69+
aie.dma_bd(%out : memref<8192xi32> offset = 0 len = 2048 sizes = [2, 4, 8, 64] strides = [2048, 512, 64, 1]) {bd_id = 0 : i32}
70+
aie.end
71+
} {issue_token = true}
72+
aiex.dma_start_task(%tout)
73+
%tin = aiex.dma_configure_task_for @of_in {
74+
aie.dma_bd(%in : memref<8192xi32> offset = 0 len = 4096 sizes = [1, 8, 16, 32] strides = [4096, 512, 32, 1]) {bd_id = 1 : i32}
75+
aie.end
76+
} {issue_token = true}
77+
aiex.dma_start_task(%tin)
78+
aiex.dma_await_task(%tout)
79+
aiex.dma_await_task(%tin)
80+
}
81+
82+
aie.runtime_sequence @task_dynamic(%in: memref<8192xi32>, %out: memref<8192xi32>, %n: i32) {
83+
%c1 = arith.constant 1 : i32
84+
%np1 = arith.addi %n, %c1 : i32
85+
aiex.npu.rtp_write(@rtp, 0, %n) : i32
86+
aiex.npu.rtp_write(@rtp, 4, %np1) : i32
87+
%tout = aiex.dma_configure_task_for @of_out {
88+
aie.dma_bd(%out : memref<8192xi32> offset = 0 len = 2048 sizes = [2, 4, 8, 64] strides = [2048, 512, 64, 1]) {bd_id = 0 : i32}
89+
aie.end
90+
} {issue_token = true}
91+
aiex.dma_start_task(%tout)
92+
%tin = aiex.dma_configure_task_for @of_in {
93+
aie.dma_bd(%in : memref<8192xi32> offset = 0 len = 4096 sizes = [1, 8, 16, 32] strides = [4096, 512, 32, 1]) {bd_id = 1 : i32}
94+
aie.end
95+
} {issue_token = true}
96+
aiex.dma_start_task(%tin)
97+
aiex.dma_await_task(%tout)
98+
aiex.dma_await_task(%tin)
99+
}
100+
}
101+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// Copyright (C) 2026 Advanced Micro Devices, Inc.
4+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5+
//
6+
//===----------------------------------------------------------------------===//
7+
//
8+
// npu1 counterpart of dma_task.mlir (the DMA-task path). Device info is baked
9+
// into the TXN header words, so the static ≡ dynamic equivalence is checked
10+
// per device generation; this file covers npu1 while dma_task.mlir covers
11+
// npu2. See that file and README.md for details.
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
// REQUIRES: peano
16+
17+
// RUN: rm -rf %t.d && mkdir -p %t.d
18+
// RUN: aie-opt --aie-substitute-shim-dma-allocations \
19+
// RUN: --aie-assign-runtime-sequence-bd-ids --aie-dma-tasks-to-npu \
20+
// RUN: --aie-dma-to-npu %s -o %t.d/lowered.mlir
21+
// RUN: aie-translate --aie-npu-to-binary -aie-output-binary=false \
22+
// RUN: -aie-sequence-name=task_static %t.d/lowered.mlir > %t.d/golden.hex
23+
// RUN: aie-translate --aie-npu-to-cpp %t.d/lowered.mlir > %t.d/gen.h
24+
// RUN: %host_clang -std=c++17 -I%S/../../../../include \
25+
// RUN: -DGEN_HDR='"%t.d/gen.h"' \
26+
// RUN: -DSTATIC_FN=generate_txn_main_task_static \
27+
// RUN: -DDYN_FN=generate_txn_main_task_dynamic -DARGVAL=4096 \
28+
// RUN: %S/Inputs/compare_main.cpp %host_link_flags -o %t.d/cmp.exe
29+
// RUN: %t.d/cmp.exe %t.d/golden.hex
30+
31+
module {
32+
aie.device(npu1) {
33+
%tile_0_0 = aie.tile(0, 0)
34+
%tile_0_2 = aie.tile(0, 2)
35+
%rtp = aie.buffer(%tile_0_2) {sym_name = "rtp", address = 49152 : i32} : memref<16xi32>
36+
aie.shim_dma_allocation @of_in (%tile_0_0, MM2S, 0)
37+
aie.shim_dma_allocation @of_out (%tile_0_0, S2MM, 0)
38+
39+
aie.runtime_sequence @task_static(%in: memref<8192xi32>, %out: memref<8192xi32>) {
40+
%c4096 = arith.constant 4096 : i32
41+
%c4097 = arith.constant 4097 : i32
42+
aiex.npu.rtp_write(@rtp, 0, %c4096) : i32
43+
aiex.npu.rtp_write(@rtp, 4, %c4097) : i32
44+
%tout = aiex.dma_configure_task_for @of_out {
45+
aie.dma_bd(%out : memref<8192xi32> offset = 0 len = 2048 sizes = [2, 4, 8, 64] strides = [2048, 512, 64, 1]) {bd_id = 0 : i32}
46+
aie.end
47+
} {issue_token = true}
48+
aiex.dma_start_task(%tout)
49+
%tin = aiex.dma_configure_task_for @of_in {
50+
aie.dma_bd(%in : memref<8192xi32> offset = 0 len = 4096 sizes = [1, 8, 16, 32] strides = [4096, 512, 32, 1]) {bd_id = 1 : i32}
51+
aie.end
52+
} {issue_token = true}
53+
aiex.dma_start_task(%tin)
54+
aiex.dma_await_task(%tout)
55+
aiex.dma_await_task(%tin)
56+
}
57+
58+
aie.runtime_sequence @task_dynamic(%in: memref<8192xi32>, %out: memref<8192xi32>, %n: i32) {
59+
%c1 = arith.constant 1 : i32
60+
%np1 = arith.addi %n, %c1 : i32
61+
aiex.npu.rtp_write(@rtp, 0, %n) : i32
62+
aiex.npu.rtp_write(@rtp, 4, %np1) : i32
63+
%tout = aiex.dma_configure_task_for @of_out {
64+
aie.dma_bd(%out : memref<8192xi32> offset = 0 len = 2048 sizes = [2, 4, 8, 64] strides = [2048, 512, 64, 1]) {bd_id = 0 : i32}
65+
aie.end
66+
} {issue_token = true}
67+
aiex.dma_start_task(%tout)
68+
%tin = aiex.dma_configure_task_for @of_in {
69+
aie.dma_bd(%in : memref<8192xi32> offset = 0 len = 4096 sizes = [1, 8, 16, 32] strides = [4096, 512, 32, 1]) {bd_id = 1 : i32}
70+
aie.end
71+
} {issue_token = true}
72+
aiex.dma_start_task(%tin)
73+
aiex.dma_await_task(%tout)
74+
aiex.dma_await_task(%tin)
75+
}
76+
}
77+
}

0 commit comments

Comments
 (0)