| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| #ifndef VIRTUAL_PE_MACHINE_HPP |
| #define VIRTUAL_PE_MACHINE_HPP |
|
|
| #include <cstdint> |
| #include <iostream> |
| #include <iomanip> |
| #include <vector> |
| #include <queue> |
| #include <cassert> |
| #include <cmath> |
|
|
| namespace VirtualPEMachine { |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| class Q44Fixed { |
| public: |
| int8_t raw; |
| |
| |
| Q44Fixed() : raw(0) {} |
| explicit Q44Fixed(int8_t r) : raw(r) {} |
| |
| |
| explicit Q44Fixed(double val) : raw(static_cast<int8_t>(val * 16.0)) {} |
| |
| |
| double toDouble() const { |
| return static_cast<double>(raw) / 16.0; |
| } |
| |
| |
| int16_t multiplyRaw(const Q44Fixed& other) const { |
| |
| |
| return static_cast<int16_t>(raw) * static_cast<int16_t>(other.raw); |
| } |
| |
| |
| friend std::ostream& operator<<(std::ostream& os, const Q44Fixed& q) { |
| os << std::fixed << std::setprecision(4) << q.toDouble() |
| << " [0x" << std::hex << std::setfill('0') << std::setw(2) |
| << (int)q.raw << std::dec << "]"; |
| return os; |
| } |
| }; |
|
|
| |
| |
| |
|
|
| class Accumulator { |
| private: |
| int16_t value; |
| |
| |
| int16_t saturate(int32_t sum) const { |
| |
| int16_t msb = (sum >> 16) & 1; |
| int16_t sign_bit = (sum >> 15) & 1; |
| |
| if (msb != sign_bit) { |
| |
| if (sum < 0) { |
| return INT16_MIN; |
| } else { |
| return INT16_MAX; |
| } |
| } |
| return static_cast<int16_t>(sum & 0xFFFF); |
| } |
| |
| public: |
| Accumulator(int16_t init = 0) : value(init) {} |
| |
| |
| void accumulate(int16_t product) { |
| int32_t sum = static_cast<int32_t>(value) + static_cast<int32_t>(product); |
| value = saturate(sum); |
| } |
| |
| |
| int16_t getValue() const { return value; } |
| |
| |
| double toDouble() const { |
| return static_cast<double>(value) / 256.0; |
| } |
| |
| |
| void reset(int16_t init = 0) { value = init; } |
| |
| |
| friend std::ostream& operator<<(std::ostream& os, const Accumulator& acc) { |
| os << std::fixed << std::setprecision(4) << acc.toDouble() |
| << " [0x" << std::hex << std::setfill('0') << std::setw(4) |
| << (int)acc.value << std::dec << "]"; |
| return os; |
| } |
| }; |
|
|
| |
| |
| |
|
|
| struct Stage1Registers { |
| bool valid = false; |
| Q44Fixed A; |
| Q44Fixed B; |
| int16_t C; |
| |
| void reset() { |
| valid = false; |
| A = Q44Fixed(static_cast<int8_t>(0)); |
| B = Q44Fixed(static_cast<int8_t>(0)); |
| C = 0; |
| } |
| }; |
|
|
| struct Stage2Registers { |
| bool valid = false; |
| int16_t product = 0; |
| int16_t C = 0; |
| |
| void reset() { |
| valid = false; |
| product = 0; |
| C = 0; |
| } |
| }; |
|
|
| struct Stage3Registers { |
| bool valid = false; |
| Accumulator acc; |
| |
| void reset() { |
| valid = false; |
| acc.reset(); |
| } |
| }; |
|
|
| |
| |
| |
|
|
| class ProcessingElement { |
| private: |
| |
| Stage1Registers s1_regs; |
| Stage2Registers s2_regs; |
| Stage3Registers s3_regs; |
| |
| |
| ProcessingElement* right_neighbor; |
| ProcessingElement* bottom_neighbor; |
| |
| |
| uint64_t cycle_count; |
| uint64_t valid_inputs; |
| uint64_t valid_outputs; |
| |
| |
| bool verbose; |
| |
| public: |
| ProcessingElement(ProcessingElement* right = nullptr, |
| ProcessingElement* bottom = nullptr) |
| : right_neighbor(right), bottom_neighbor(bottom), |
| cycle_count(0), valid_inputs(0), valid_outputs(0), |
| verbose(false) {} |
| |
| |
| |
| |
| |
| void clock(bool valid_in, const Q44Fixed& A_in, const Q44Fixed& B_in, |
| int16_t C_in) { |
| |
| if (verbose) { |
| std::cout << "=== CLOCK #" << cycle_count << " ===" << std::endl; |
| } |
| |
| |
| |
| if (verbose && s3_regs.valid) { |
| std::cout << "S3: valid=" << s3_regs.valid |
| << " accumulator=" << s3_regs.acc << std::endl; |
| } |
| |
| |
| if (s2_regs.valid) { |
| s3_regs.valid = true; |
| s3_regs.acc.accumulate(s2_regs.product); |
| |
| if (verbose) { |
| std::cout << "S2→S3: product=" << s2_regs.product |
| << " + C=" << s2_regs.C |
| << " → result=" << s3_regs.acc << std::endl; |
| } |
| } else { |
| s3_regs.valid = false; |
| } |
| |
| |
| if (s1_regs.valid) { |
| s2_regs.valid = true; |
| s2_regs.product = s1_regs.A.multiplyRaw(s1_regs.B); |
| s2_regs.C = s1_regs.C; |
| |
| if (verbose) { |
| std::cout << "S1→S2: " << s1_regs.A << " × " << s1_regs.B |
| << " = " << s2_regs.product << std::endl; |
| } |
| } else { |
| s2_regs.valid = false; |
| } |
| |
| |
| s1_regs.valid = valid_in; |
| if (valid_in) { |
| s1_regs.A = A_in; |
| s1_regs.B = B_in; |
| s1_regs.C = C_in; |
| valid_inputs++; |
| |
| if (verbose) { |
| std::cout << "IN→S1: valid=" << valid_in |
| << " A=" << A_in << " B=" << B_in << " C_in=" << C_in << std::endl; |
| } |
| } |
| |
| if (s3_regs.valid) { |
| valid_outputs++; |
| } |
| |
| cycle_count++; |
| } |
| |
| |
| |
| |
| |
| bool getValidOut() const { |
| return s3_regs.valid; |
| } |
| |
| Q44Fixed getMatrixAOut() const { |
| return s1_regs.A; |
| } |
| |
| int16_t getMatrixCOut() const { |
| return s3_regs.acc.getValue(); |
| } |
| |
| |
| |
| |
| |
| void printStatistics() const { |
| std::cout << "\n=== PE Statistics ===" << std::endl; |
| std::cout << "Cycles executed: " << cycle_count << std::endl; |
| std::cout << "Valid inputs: " << valid_inputs << std::endl; |
| std::cout << "Valid outputs: " << valid_outputs << std::endl; |
| std::cout << "Latency (cycles S1→S3): 3" << std::endl; |
| } |
| |
| void setVerbose(bool v) { verbose = v; } |
| |
| |
| void reset() { |
| s1_regs.reset(); |
| s2_regs.reset(); |
| s3_regs.reset(); |
| cycle_count = 0; |
| valid_inputs = 0; |
| valid_outputs = 0; |
| } |
| }; |
|
|
| |
| |
| |
|
|
| class SystolicArray { |
| private: |
| std::vector<std::vector<ProcessingElement>> grid; |
| size_t rows, cols; |
| |
| public: |
| SystolicArray(size_t r, size_t c) : rows(r), cols(c) { |
| |
| grid.resize(rows, std::vector<ProcessingElement>(cols)); |
| |
| |
| for (size_t i = 0; i < rows; ++i) { |
| for (size_t j = 0; j < cols; ++j) { |
| ProcessingElement* right = (j < cols - 1) ? &grid[i][j+1] : nullptr; |
| ProcessingElement* bottom = (i < rows - 1) ? &grid[i+1][j] : nullptr; |
| |
| grid[i][j] = ProcessingElement(right, bottom); |
| } |
| } |
| } |
| |
| ProcessingElement& getPE(size_t r, size_t c) { |
| assert(r < rows && c < cols); |
| return grid[r][c]; |
| } |
| |
| size_t getRows() const { return rows; } |
| size_t getCols() const { return cols; } |
| |
| |
| void clockAll() { |
| for (size_t i = 0; i < rows; ++i) { |
| for (size_t j = 0; j < cols; ++j) { |
| |
| |
| grid[i][j].clock(false, Q44Fixed(static_cast<int8_t>(0)), Q44Fixed(static_cast<int8_t>(0)), 0); |
| } |
| } |
| } |
| }; |
|
|
| |
| |
| |
|
|
| class PEVerifier { |
| public: |
| |
| static bool verifyFixedPointArithmetic() { |
| Q44Fixed a(2.0); |
| Q44Fixed b(3.5); |
| |
| int16_t product = a.multiplyRaw(b); |
| |
| int16_t expected = static_cast<int16_t>(7.0 * 256); |
| |
| bool pass = (product == expected); |
| std::cout << "FixedPoint Arithmetic: " << (pass ? "PASS" : "FAIL") |
| << " (product=" << product << ", expected=" << expected << ")" << std::endl; |
| return pass; |
| } |
| |
| |
| static bool verifySaturation() { |
| Accumulator acc(0); |
| |
| |
| acc.accumulate(INT16_MAX); |
| acc.accumulate(1000); |
| |
| bool pass = (acc.getValue() == INT16_MAX); |
| std::cout << "Saturation Logic: " << (pass ? "PASS" : "FAIL") |
| << " (value=" << acc.getValue() << ")" << std::endl; |
| return pass; |
| } |
| |
| |
| static bool verifyPipelineLatency() { |
| ProcessingElement pe; |
|
|
| Q44Fixed a(1.0), b(2.0); |
| int16_t c_in = 0; |
|
|
| |
| |
| |
| |
| |
| pe.clock(true, a, b, c_in); |
| if (pe.getValidOut()) return false; |
|
|
| |
| pe.clock(false, Q44Fixed(static_cast<int8_t>(0)), Q44Fixed(static_cast<int8_t>(0)), 0); |
| if (pe.getValidOut()) return false; |
|
|
| |
| pe.clock(false, Q44Fixed(static_cast<int8_t>(0)), Q44Fixed(static_cast<int8_t>(0)), 0); |
| if (!pe.getValidOut()) return false; |
|
|
| std::cout << "Pipeline Latency (3-stage, 2-cycle): PASS" << std::endl; |
| return true; |
| } |
| |
| |
| static bool verifyMAC() { |
| ProcessingElement pe; |
| |
| Q44Fixed a(2.0); |
| Q44Fixed b(3.0); |
| int16_t c_in = 0; |
| |
| pe.clock(true, a, b, c_in); |
| pe.clock(false, Q44Fixed(static_cast<int8_t>(0)), Q44Fixed(static_cast<int8_t>(0)), 0); |
| pe.clock(false, Q44Fixed(static_cast<int8_t>(0)), Q44Fixed(static_cast<int8_t>(0)), 0); |
| pe.clock(false, Q44Fixed(static_cast<int8_t>(0)), Q44Fixed(static_cast<int8_t>(0)), 0); |
| |
| int16_t result = pe.getMatrixCOut(); |
| int16_t expected = static_cast<int16_t>(2.0 * 3.0 * 256); |
| |
| bool pass = (result == expected); |
| std::cout << "MAC Numerical Correctness: " << (pass ? "PASS" : "FAIL") |
| << " (result=" << result << ", expected=" << expected << ")" << std::endl; |
| return pass; |
| } |
| }; |
|
|
| } |
|
|
| #endif |
|
|