diff --git a/include/aie-c/Translation.h b/include/aie-c/Translation.h index 6f791f8889c..1b67eef4e09 100644 --- a/include/aie-c/Translation.h +++ b/include/aie-c/Translation.h @@ -40,6 +40,8 @@ MLIR_CAPI_EXPORTED MlirLogicalResult aieTranslateToCDODirect( bool xaieDebug, bool enableCores); MLIR_CAPI_EXPORTED MlirOperation aieTranslateBinaryToTxn(MlirContext ctx, MlirStringRef binary); +MLIR_CAPI_EXPORTED MlirOperation aieTranslateBinaryToControlPackets( + MlirContext ctx, MlirStringRef binary, int device); struct AieRtControl { void *ptr; diff --git a/include/aie/Conversion/AIEToConfiguration/AIEToConfiguration.h b/include/aie/Conversion/AIEToConfiguration/AIEToConfiguration.h index f07f0fa0aa0..fd4b930596e 100644 --- a/include/aie/Conversion/AIEToConfiguration/AIEToConfiguration.h +++ b/include/aie/Conversion/AIEToConfiguration/AIEToConfiguration.h @@ -17,6 +17,7 @@ namespace xilinx::AIE { +enum class AIEDevice : uint32_t; class DeviceOp; std::unique_ptr> @@ -29,6 +30,10 @@ std::optional convertTransactionBinaryToMLIR(mlir::MLIRContext *ctx, std::vector &binary); +std::optional convertControlPacketBinaryToMLIR( + mlir::MLIRContext *ctx, std::vector &binary, + AIEDevice device = static_cast(4)); /* npu1 */ + } // namespace xilinx::AIE #endif // AIE_CONVERSION_AIETOCONFIGURATION_AIETOCONFIGURATION_H diff --git a/lib/CAPI/Translation.cpp b/lib/CAPI/Translation.cpp index ff47f9ad9fc..4e582f1c911 100644 --- a/lib/CAPI/Translation.cpp +++ b/lib/CAPI/Translation.cpp @@ -94,6 +94,17 @@ MlirOperation aieTranslateBinaryToTxn(MlirContext ctx, MlirStringRef binary) { return wrap(mod->getOperation()); } +MlirOperation aieTranslateBinaryToControlPackets(MlirContext ctx, + MlirStringRef binary, + int device) { + std::vector binaryData(binary.data, binary.data + binary.length); + auto mod = convertControlPacketBinaryToMLIR(unwrap(ctx), binaryData, + static_cast(device)); + if (!mod) + return wrap(ModuleOp().getOperation()); + return wrap(mod->getOperation()); +} + MlirStringRef aieTranslateNpuToBinary(MlirOperation moduleOp, MlirStringRef deviceNameMlir, MlirStringRef sequenceNameMlir) { diff --git a/lib/Conversion/AIEToConfiguration/AIEToConfiguration.cpp b/lib/Conversion/AIEToConfiguration/AIEToConfiguration.cpp index 65a1b43e28c..b982ec6da26 100644 --- a/lib/Conversion/AIEToConfiguration/AIEToConfiguration.cpp +++ b/lib/Conversion/AIEToConfiguration/AIEToConfiguration.cpp @@ -16,6 +16,7 @@ #include "aie/Targets/AIERT.h" #include "llvm/Support/Debug.h" +#include "llvm/Support/Format.h" #include extern "C" { @@ -98,8 +99,134 @@ struct TxnLoadPdiHeader { uint32_t size; uint64_t address; }; + +// A ControlPacketOperation encapsulates a parsed control packet including +// stream header, control packet header, and optional data payload. +struct ControlPacketOperation { + uint32_t streamHeader; // word 0: parity + pkt_type + pkt_id + uint32_t packetHeader; // word 1: parity + stream_id + opcode + beats + addr + std::vector data; // payload words (if opcode is write/blockwrite) + + // Decoded fields + uint8_t pktType; + uint8_t pktId; + uint8_t streamId; + uint8_t opcode; + uint8_t beats; + uint32_t address; +}; + } // namespace +// Check even parity bit (bit 31) +// The parity bit (bit 31) is set to 1 if the popcount of bits[30:0] is even +static bool checkParity(uint32_t word) { + uint32_t p = 0; + uint32_t w = word & 0x7FFFFFFF; // mask off parity bit + while (w) { + p += w & 1; + w >>= 1; + } + // Parity bit should be 1 if popcount is even, 0 if odd + bool expectedParity = (p % 2) == 0; + bool actualParity = (word >> 31) & 1; + return expectedParity == actualParity; +} + +// Parse a control packet binary blob. On success return a vector of parsed +// control packet operations. On failure return std::nullopt. +static std::optional> +parseControlPacket(const std::vector &data) { + std::vector ops; + + size_t i = 0; + + auto requireBytes = [&](size_t offset, size_t length) -> bool { + if (offset + length > data.size()) { + llvm::errs() << "Control packet binary truncated\n"; + return false; + } + return true; + }; + + auto read32 = [&](size_t offset) -> uint32_t { + uint32_t value; + std::memcpy(&value, data.data() + offset, sizeof(uint32_t)); + return value; + }; + + while (i + 8 <= data.size()) { // Need at least 2 words (stream hdr + pkt hdr) + ControlPacketOperation op; + + // Read stream header (word 0) + op.streamHeader = read32(i); + + // Read control packet header (word 1) + op.packetHeader = read32(i + 4); + + // Verify parity + if (!checkParity(op.streamHeader)) { + llvm::errs() << "Stream header parity check failed at offset " << i + << "\n"; + return std::nullopt; + } + if (!checkParity(op.packetHeader)) { + llvm::errs() << "Packet header parity check failed at offset " << i + 4 + << "\n"; + return std::nullopt; + } + + // Decode stream header fields + op.pktType = (op.streamHeader >> 12) & 0x7; + op.pktId = op.streamHeader & 0xFF; + + // Decode control packet header fields + op.streamId = (op.packetHeader >> 24) & 0x7F; + op.opcode = (op.packetHeader >> 22) & 0x3; + op.beats = (op.packetHeader >> 20) & 0x3; + op.address = op.packetHeader & 0xFFFFF; + + i += 8; // consumed 2 words + + LLVM_DEBUG(llvm::dbgs() << "Control packet at offset " << (i - 8) + << ": opcode=" << static_cast(op.opcode) + << " stream_id=" << static_cast(op.streamId) + << " addr=0x" << llvm::format("%05X", op.address) + << " beats=" << static_cast(op.beats) << "\n"); + + // Read data payload if present (opcode 0x0=write or 0x2=blockwrite) + if (op.opcode == 0x0 || op.opcode == 0x2) { + uint32_t numDataWords = op.beats + 1; + if (!requireBytes(i, numDataWords * 4)) { + llvm::errs() << "Truncated data payload\n"; + return std::nullopt; + } + + op.data.resize(numDataWords); + for (uint32_t j = 0; j < numDataWords; j++) { + op.data[j] = read32(i + j * 4); + } + i += numDataWords * 4; + + LLVM_DEBUG(llvm::dbgs() << " Data: ["; for (size_t j = 0; + j < op.data.size(); j++) { + if (j > 0) + llvm::dbgs() << ", "; + llvm::dbgs() << op.data[j]; + } llvm::dbgs() << "]\n"); + } + + ops.push_back(std::move(op)); + } + + if (i != data.size()) { + llvm::errs() << "Warning: " << (data.size() - i) + << " bytes remaining after parsing control packets\n"; + } + + return ops; +} + // Parse a TXN binary blob. On success return the number of columns from the // header and a vector of parsed operations. On failure return std::nullopt. static std::optional @@ -748,6 +875,89 @@ xilinx::AIE::convertTransactionBinaryToMLIR(mlir::MLIRContext *ctx, return module; } +// Convert (disassemble) a control packet binary to MLIR. On success return a +// new ModuleOp containing a DeviceOp containing a runtime sequence with the +// control packet binary encoded as a sequence of aiex.control_packet +// operations. On failure return std::nullopt. +std::optional xilinx::AIE::convertControlPacketBinaryToMLIR( + mlir::MLIRContext *ctx, std::vector &binary, AIEDevice device) { + + // parse the binary + auto maybeOps = parseControlPacket(binary); + if (!maybeOps) { + llvm::errs() << "Failed to parse control packet binary\n"; + return std::nullopt; + } + std::vector operations = *maybeOps; + + auto loc = mlir::UnknownLoc::get(ctx); + + // create a new ModuleOp and set the insertion point + auto module = ModuleOp::create(loc); + OpBuilder builder(module.getBodyRegion()); + builder.setInsertionPointToStart(module.getBody()); + + // Create aie.device with specified device type + auto deviceAttr = AIEDeviceAttr::get(ctx, device); + auto deviceOp = builder.create( + loc, deviceAttr, StringAttr::get(builder.getContext())); + deviceOp.getRegion().emplaceBlock(); + DeviceOp::ensureTerminator(deviceOp.getBodyRegion(), builder, loc); + builder.setInsertionPointToStart(deviceOp.getBody()); + + // Create tiles and set controller_id attributes based on packet info + // Group operations by (pktType, pktId) to determine which tile they target + std::map, std::pair> tileMap; + for (const auto &op : operations) { + auto key = std::make_pair(op.pktType, op.pktId); + if (tileMap.find(key) == tileMap.end()) { + // Extract col/row from address using target model + const AIETargetModel &targetModel = deviceOp.getTargetModel(); + uint32_t colInt = (op.address >> targetModel.getColumnShift()) & 0x1f; + uint32_t rowInt = (op.address >> targetModel.getRowShift()) & 0x1f; + tileMap[key] = std::make_pair(colInt, rowInt); + + // Create tile and set controller_id attribute + auto tile = TileOp::getOrCreate(builder, deviceOp, colInt, rowInt); + auto packetInfoAttr = + AIE::PacketInfoAttr::get(builder.getContext(), op.pktType, op.pktId); + tile->setAttr("controller_id", packetInfoAttr); + } + } + + // Create aiex.runtime_sequence + std::string seq_name = "configure"; + StringAttr seq_sym_name = builder.getStringAttr(seq_name); + auto seq = builder.create(loc, seq_sym_name); + seq.getBody().push_back(new Block); + + // Create control packet ops + builder.setInsertionPointToStart(&seq.getBody().front()); + for (const auto &op : operations) { + IntegerAttr lengthAttr; + DenseI32ArrayAttr dataAttr; + + if (op.opcode == 0x0 || op.opcode == 0x2) { + // Write or blockwrite - has data payload + SmallVector dataVec; + for (uint32_t val : op.data) { + dataVec.push_back(static_cast(val)); + } + dataAttr = DenseI32ArrayAttr::get(ctx, ArrayRef(dataVec)); + } else if (op.opcode == 0x1) { + // Read - has length but no data + lengthAttr = builder.getI32IntegerAttr(op.beats + 1); + } + + builder.create( + loc, builder.getUI32IntegerAttr(op.address), lengthAttr, + builder.getI32IntegerAttr(op.opcode), + builder.getI32IntegerAttr(op.streamId), dataAttr); + } + + return module; +} + static LogicalResult convertAIEToConfiguration(AIE::DeviceOp device, StringRef clElfDir, OutputType outputType) { diff --git a/python/AIEMLIRModule.cpp b/python/AIEMLIRModule.cpp index 59eeafe7842..5042b54841b 100644 --- a/python/AIEMLIRModule.cpp +++ b/python/AIEMLIRModule.cpp @@ -133,6 +133,15 @@ NB_MODULE(_aie, m) { }, "ctx"_a, "binary"_a); + m.def( + "control_packets_binary_to_mlir", + [](MlirContext ctx, nb::bytes bytes, int device) { + MlirStringRef bin = {static_cast(bytes.data()), + bytes.size()}; + return aieTranslateBinaryToControlPackets(ctx, bin, device); + }, + "ctx"_a, "binary"_a, "device"_a = 4); /* npu1 */ + m.def( "translate_npu_to_binary", [](MlirOperation op, const std::string &device_name, diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index 97e9b161a53..de4470b69e7 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -110,6 +110,7 @@ declare_mlir_python_sources(AIEPythonSources.Compiler SOURCES_GLOB compiler/aiecc/*.py compiler/txn2mlir/*.py + compiler/pkt2mlir/*.py ) if (AIE_ENABLE_XRT_PYTHON_BINDINGS) @@ -424,8 +425,9 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/compiler/aiecc.py ${CMAKE_CURRENT_SOURCE_DIR}/compiler/txn2mlir.py + ${CMAKE_CURRENT_SOURCE_DIR}/compiler/pkt2mlir.py ${CMAKE_BINARY_DIR}/bin ) # during install -install(PROGRAMS compiler/aiecc.py compiler/txn2mlir.py DESTINATION bin) +install(PROGRAMS compiler/aiecc.py compiler/txn2mlir.py compiler/pkt2mlir.py DESTINATION bin) diff --git a/python/compiler/pkt2mlir.py b/python/compiler/pkt2mlir.py new file mode 100644 index 00000000000..9493b388460 --- /dev/null +++ b/python/compiler/pkt2mlir.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +# +# This file is licensed under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# Copyright (C) 2025, Advanced Micro Devices, Inc. All rights reserved. + +from aie.compiler.pkt2mlir.main import main + +if __name__ == "__main__": + main() diff --git a/python/compiler/pkt2mlir/README.md b/python/compiler/pkt2mlir/README.md new file mode 100644 index 00000000000..7fdf70196a8 --- /dev/null +++ b/python/compiler/pkt2mlir/README.md @@ -0,0 +1,127 @@ +# pkt2mlir - Control Packet Binary to MLIR Disassembler + +`pkt2mlir.py` is a tool that converts AIE NPU control packet binaries into human-readable MLIR format. This is useful for debugging, inspecting compiled configurations, and understanding the control packet operations sent to the NPU. + +## Overview + +The tool parses binary control packet files generated by `aie-translate --aie-ctrlpkt-to-bin` and reconstructs the equivalent MLIR module containing: +- Device configuration (`aie.device`) +- Tile controller_id attributes (`PacketInfoAttr`) +- Runtime sequences with control packet operations (`aiex.control_packet`) + +## Usage + +```bash +pkt2mlir.py -f [-d ] +``` + +### Options + +- `-f`, `-file FILE` : Input control packet binary file (required) +- `-d`, `-device DEVICE` : Target AIE device type (default: npu1) + - Supported devices: xcvc1902, xcve2302, xcve2802, npu1, npu1_1col, npu1_2col, npu1_3col, npu2, npu2_1col, npu2_2col, npu2_3col, npu2_4col, npu2_5col, npu2_6col, npu2_7col +- `-h`, `--help` : Show help message and exit + +## Examples + +### Basic Control Packet Operations Round-trip + +Generate control packet binary and disassemble it: + +```bash +# Create a simple MLIR file with control packets +cat > control_packets.mlir << 'EOF' +module { + aie.device(npu1) { + %tile00 = aie.tile(0, 0) {controller_id = #aie.packet_info} + + aiex.runtime_sequence() { + aiex.control_packet {address = 0x0001F000 : ui32, opcode = 0 : i32, stream_id = 0 : i32, data = array} + aiex.control_packet {address = 0x0001F020 : ui32, opcode = 2 : i32, stream_id = 9 : i32, data = array} + aiex.control_packet {address = 0x00000400 : ui32, opcode = 1 : i32, stream_id = 2 : i32, length = 4 : i32} + } + } +} +EOF + +# Compile to binary +aie-translate --aie-ctrlpkt-to-bin control_packets.mlir -o control_packets.bin +``` + +View the raw control packet binary structure: + +```bash +# Inspect binary structure (2 word header + optional data for each packet) +hexdump -C control_packets.bin +``` + +``` +00000000 00 20 00 03 00 1f 00 00 02 00 00 00 00 20 00 03 |. .......... ..| +00000010 09 b1 f0 20 03 00 00 00 04 00 00 00 05 00 00 00 |... ............| +00000020 06 00 00 00 00 20 00 03 02 70 04 00 |..... ...p..| +``` + +Translate back to MLIR: + +```bash +pkt2mlir.py -f control_packets.bin +``` + +Or specify a different device type: + +```bash +pkt2mlir.py -f control_packets.bin -d npu1 +``` + +Output: + +```mlir +module { + aie.device(npu1) { + %shim_noc_tile_0_0 = aie.tile(0, 0) {controller_id = #aie.packet_info} + + aiex.runtime_sequence @configure() { + aiex.control_packet {address = 126976 : ui32, data = array, opcode = 0 : i32, stream_id = 0 : i32} + aiex.control_packet {address = 127008 : ui32, data = array, opcode = 2 : i32, stream_id = 9 : i32} + aiex.control_packet {address = 1024 : ui32, length = 4 : i32, opcode = 1 : i32, stream_id = 2 : i32} + } + } +} +``` + +## Control Packet Binary Format + +Each control packet in the binary consists of: + +1. **Stream Header** (32 bits): + - Bit [31]: Even parity bit + - Bits [14:12]: Packet type (3 bits) + - Bits [7:0]: Packet ID (8 bits) + +2. **Control Packet Header** (32 bits): + - Bit [31]: Even parity bit + - Bits [30:24]: Stream ID (7 bits) + - Bits [23:22]: Opcode (2 bits) + - `0x0` = Write + - `0x1` = Read + - `0x2` = Block write + - Bits [21:20]: Beats (size-1, 2 bits) + - Bits [19:0]: Address (20 bits) + +3. **Data Payload** (optional, 32-bit words): + - Present for write (opcode 0x0) and block write (opcode 0x2) + - Number of words = beats + 1 + +## Use Cases + +- **Debugging**: Inspect what control packets are being sent to the NPU +- **Verification**: Compare generated binaries against expected values +- **Reverse Engineering**: Understand control packet configurations +- **Testing**: Validate round-trip conversion (MLIR → binary → MLIR) +- **Documentation**: Generate human-readable documentation from binaries + +## See Also + +- `txn2mlir.py` - Transaction binary to MLIR disassembler +- `aie-translate --aie-ctrlpkt-to-bin` - Generate control packet binaries +- `aie-translate --aie-npu-to-binary` - Generate transaction binaries diff --git a/python/compiler/pkt2mlir/main.py b/python/compiler/pkt2mlir/main.py new file mode 100644 index 00000000000..813942a89cf --- /dev/null +++ b/python/compiler/pkt2mlir/main.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# +# This file is licensed under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# Copyright (C) 2025, Advanced Micro Devices, Inc. All rights reserved. + +from aie.ir import * +from aie.dialects.aie import * + +import argparse + + +def main(): + # Parse arguments + parser = argparse.ArgumentParser( + description="Convert AIE control packet binaries to MLIR" + ) + parser.add_argument( + "-file", + "-f", + type=argparse.FileType("rb"), + required=True, + help="Input control packet binary file", + ) + parser.add_argument( + "-device", + "-d", + type=str, + default="npu1", + help="Target AIE device type (default: npu1)", + ) + + args = parser.parse_args() + + # Read the data from the file + data = args.file.read() + + # Get the device enum value + device_value = getattr(AIEDevice, args.device) + + with Context() as ctx: + module = control_packets_binary_to_mlir(ctx, data, device_value) + + print(str(module)) + + +if __name__ == "__main__": + main() diff --git a/python/dialects/aie.py b/python/dialects/aie.py index 9f60efb9c9a..0d6a17bab4e 100644 --- a/python/dialects/aie.py +++ b/python/dialects/aie.py @@ -38,6 +38,7 @@ translate_aie_vec_to_cpp, translate_mlir_to_llvmir, transaction_binary_to_mlir, + control_packets_binary_to_mlir, ) from ..extras import types as T from ..extras.meta import region_op