| #include "sim/kelvin_vector_memory_instructions.h" |
| |
| #include <assert.h> |
| |
| #include <algorithm> |
| #include <cstdint> |
| #include <limits> |
| #include <utility> |
| #include <vector> |
| |
| #include "sim/test/kelvin_vector_instructions_test_base.h" |
| #include "googletest/include/gtest/gtest.h" |
| #include "absl/functional/bind_front.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/strings/string_view.h" |
| #include "absl/types/span.h" |
| #include "mpact/sim/generic/instruction.h" |
| |
| // This file contains the tests for testing kelvin vector memory instructions. |
| |
| namespace { |
| |
| using mpact::sim::generic::Instruction; |
| |
| // Semantic functions. |
| using kelvin::sim::KelvinAcSet; |
| using kelvin::sim::KelvinGetVl; |
| using kelvin::sim::KelvinVcGet; |
| using kelvin::sim::KelvinVLd; |
| using kelvin::sim::KelvinVLdRegWrite; |
| using kelvin::sim::KelvinVSt; |
| using kelvin::sim::KelvinVStQ; |
| |
| class KelvinVectorMemoryInstructionsTest |
| : public kelvin::sim::test::KelvinVectorInstructionsTestBase { |
| public: |
| template <typename T> |
| void MemoryLoadStoreOpTestHelper(absl::string_view name, bool has_length, |
| bool has_stride, bool strip_mine, |
| bool post_increment, bool x_variant, |
| bool is_load, bool is_quad) { |
| InstructionPtr child_instruction( |
| new Instruction(next_instruction_address_, state_), |
| [](Instruction *inst) { inst->DecRef(); }); |
| child_instruction->set_size(4); |
| auto instruction = CreateInstruction(); |
| |
| if (is_load) { |
| child_instruction->set_semantic_function( |
| absl::bind_front(&KelvinVLdRegWrite<T>, strip_mine)); |
| instruction->set_semantic_function( |
| absl::bind_front(&KelvinVLd<T>, has_length, has_stride, strip_mine)); |
| instruction->AppendChild(child_instruction.get()); |
| } else { |
| if (is_quad) { |
| instruction->set_semantic_function( |
| absl::bind_front(&KelvinVStQ<T>, strip_mine)); |
| } else { |
| instruction->set_semantic_function(absl::bind_front( |
| &KelvinVSt<T>, has_length, has_stride, strip_mine)); |
| } |
| } |
| |
| // Setup source and child instruction operands. |
| const uint32_t num_ops = strip_mine ? 4 : 1; |
| if (is_load) { |
| AppendVectorRegisterOperands( |
| child_instruction.get(), num_ops, 1 /* src1_widen_factor */, {}, {}, |
| false /* widen_dst */, {kelvin::sim::test::kVd}); |
| } else { // Store |
| AppendVectorRegisterOperands( |
| instruction.get(), num_ops, 1 /* src1_widen_factor */, |
| kelvin::sim::test::kVd, {}, false /* widen_dst */, {}); |
| } |
| AppendRegisterOperands(instruction.get(), {kelvin::sim::test::kRs1Name}, |
| {}); |
| if (!x_variant) { |
| AppendRegisterOperands(instruction.get(), {kelvin::sim::test::kRs2Name}, |
| {}); |
| } |
| |
| if (post_increment) { |
| AppendRegisterOperands(instruction.get(), {}, |
| {kelvin::sim::test::kRs1Name}); |
| } |
| |
| // x variant can't have length or stride fields. |
| if (x_variant && (has_length || has_stride)) { |
| GTEST_FAIL(); |
| } |
| |
| // xx variant can't have no length, no stride, and no post_increment |
| // encoding |
| if (!x_variant && !has_length && !has_stride && !post_increment) { |
| GTEST_FAIL(); |
| } |
| |
| // length and stride fields can't coexist without post_increment |
| if (has_length && has_stride && !post_increment) { |
| GTEST_FAIL(); |
| } |
| |
| // Quad store need to have stride specified and no length |
| if (is_quad && is_load) { |
| GTEST_FAIL(); |
| } |
| if ((is_quad && has_length) || (is_quad && !has_stride)) { |
| GTEST_FAIL(); |
| } |
| const uint32_t vector_length_in_bytes = state_->vector_length() / 8; |
| const uint32_t vd_size = vector_length_in_bytes / sizeof(T); |
| const uint32_t len_or_strides[] = {0, 1, vd_size - 1, |
| vd_size, 2 * vd_size, 4 * vd_size}; |
| |
| // Check with different values for length and stride if applicable. |
| for (int test = 0; |
| test < (has_length || has_stride ? std::size(len_or_strides) : 1); |
| test++) { |
| // Store stride can't be smaller than vd_size |
| if ((is_quad && len_or_strides[test] < vd_size / 4) || |
| (!is_load && has_stride && len_or_strides[test] < vd_size)) { |
| continue; |
| } |
| // Set input register values. |
| SetRegisterValues<uint32_t>( |
| {{kelvin::sim::test::kRs1Name, kelvin::sim::test::kDataLoadAddress}}); |
| |
| if (!x_variant) { |
| SetRegisterValues<uint32_t>( |
| {{kelvin::sim::test::kRs2Name, len_or_strides[test]}}); |
| } |
| |
| // Fill vector register(s) with random values. |
| std::vector<T> vd_value(vector_length_in_bytes / sizeof(T) * num_ops); |
| auto vd_span = absl::Span<T>(vd_value); |
| FillArrayWithRandomValues<T>(vd_span); |
| for (int i = 0; i < num_ops; i++) { |
| auto vd_name = absl::StrCat("v", kelvin::sim::test::kVd + i); |
| SetVectorRegisterValues<T>( |
| {{vd_name, vd_span.subspan(vd_size * i, vd_size)}}); |
| } |
| |
| // Execute instruction. |
| instruction->Execute(); |
| |
| // Compute memory values. For load test it is the expected output; for |
| // store test it is the actual output. |
| std::vector<T> memory_values(vd_size * num_ops); |
| uint32_t addr = kelvin::sim::test::kDataLoadAddress; |
| uint32_t rs2_value = len_or_strides[test]; |
| uint32_t count = vd_size * num_ops; |
| if (has_length) { |
| count = std::min(count, rs2_value); |
| } |
| uint32_t left = count; |
| for (int op_num = 0; op_num < num_ops; op_num++) { |
| const int n = std::min(vd_size, left); |
| if (is_quad) { |
| const uint32_t quad_size = vd_size / 4; |
| for (int i = 0; i < 4; ++i) { |
| for (int j = 0; j < quad_size; ++j) { |
| memory_values[op_num * vd_size + i * quad_size + j] = |
| GetSavedMemoryValue<T>(addr + |
| (i * quad_size + j) * sizeof(T)); |
| } |
| // Stride increase per quad_size. |
| addr += rs2_value * sizeof(T); |
| } |
| } else { |
| for (int i = 0; i < vd_size; ++i) { |
| if (is_load) { |
| memory_values[op_num * vd_size + i] = |
| i < n ? GetDefaultMemoryValue<T>(addr + i * sizeof(T)) : 0; |
| } else { |
| memory_values[op_num * vd_size + i] = |
| i < n ? GetSavedMemoryValue<T>(addr + i * sizeof(T)) : 0; |
| } |
| } |
| left -= n; |
| if (has_stride) { |
| addr += rs2_value * sizeof(T); |
| } else { |
| addr += n * sizeof(T); |
| } |
| } |
| } |
| |
| uint32_t expected_rs1_value = kelvin::sim::test::kDataLoadAddress; |
| if (post_increment && count) { |
| if (has_length && has_stride) { // .tp |
| expected_rs1_value += vd_size * sizeof(T); |
| } else if (!has_length && !has_stride && x_variant) { // .p.x |
| expected_rs1_value += vd_size * sizeof(T) * num_ops; |
| } else if (has_length) { // .lp |
| expected_rs1_value += count * sizeof(T); |
| } else if (has_stride) { // .sp |
| const uint32_t quad_scale = is_quad ? 4 : 1; |
| expected_rs1_value += rs2_value * sizeof(T) * num_ops * quad_scale; |
| } else { // .p.xx |
| expected_rs1_value += rs2_value * sizeof(T); |
| } |
| } |
| |
| // Check result |
| left = count; |
| for (int op_num = 0; op_num < num_ops; op_num++) { |
| auto vreg_num = kelvin::sim::test::kVd + op_num; |
| auto test_vreg = vreg_[vreg_num]; |
| auto vreg_span = test_vreg->data_buffer()->Get<T>(); |
| if (is_load) { |
| for (int element_index = 0; element_index < vd_size; |
| element_index++) { |
| auto vreg_element_index = op_num * vd_size + element_index; |
| EXPECT_EQ(memory_values[vreg_element_index], |
| vreg_span[element_index]) |
| << absl::StrCat(name, "[", vreg_element_index, "] != reg[", |
| vreg_num, "*", element_index, "]"); |
| } |
| } else { // Store |
| const int n = std::min(vd_size, left); |
| for (int element_index = 0; |
| element_index < vd_size && element_index < n; element_index++) { |
| auto total_element_index = op_num * vd_size + element_index; |
| EXPECT_EQ(memory_values[total_element_index], |
| vreg_span[element_index]) |
| << absl::StrCat(name, " mem at ", total_element_index, |
| " != vreg[", vreg_num, "][", element_index, |
| "]"); |
| } |
| left -= n; |
| } |
| } |
| |
| if (post_increment) { |
| // Check rs1 value. |
| auto *reg = state_ |
| ->GetRegister<kelvin::sim::test::RV32Register>( |
| kelvin::sim::test::kRs1Name) |
| .first; |
| EXPECT_EQ(expected_rs1_value, reg->data_buffer()->Get<uint32_t>()[0]) |
| << absl::StrCat(name, " post incremented rs1 is incorrect."); |
| } |
| } |
| } |
| |
| template <typename T> |
| void MemoryLoadStoreOpTestHelper(absl::string_view name, bool is_load) { |
| constexpr bool kNoLength = false; |
| constexpr bool kLength = true; |
| constexpr bool kNoStride = false; |
| constexpr bool kStride = true; |
| constexpr bool kPostIncrement = true; |
| constexpr bool kXVariant = true; |
| constexpr bool kNotXVariant = false; |
| constexpr bool kNotQuad = false; |
| |
| const auto name_with_type = absl::StrCat(name, KelvinTestTypeSuffix<T>()); |
| |
| for (auto strip_mine : {false, true}) { |
| for (auto post_increment : {false, true}) { |
| // .x variants. |
| auto subname = absl::StrCat(name_with_type, post_increment ? "P" : "", |
| "X", strip_mine ? "M" : ""); |
| MemoryLoadStoreOpTestHelper<T>(subname, kNoLength, kNoStride, |
| strip_mine, post_increment, kXVariant, |
| is_load, kNotQuad); |
| } |
| // .xx variants |
| for (auto len_stride_post : |
| {std::tuple(false, false, true), std::tuple(false, true, false), |
| std::tuple(false, true, true), std::tuple(true, false, false), |
| std::tuple(true, false, true)}) { |
| auto has_length = std::get<0>(len_stride_post); |
| auto has_stride = std::get<1>(len_stride_post); |
| auto post_increment = std::get<2>(len_stride_post); |
| auto subname = absl::StrCat(name_with_type, |
| has_length ? "L" |
| : has_stride ? "S" |
| : "", |
| post_increment ? "P" : "", "XX", |
| strip_mine ? "M" : ""); |
| MemoryLoadStoreOpTestHelper<T>(subname, has_length, has_stride, |
| strip_mine, post_increment, kNotXVariant, |
| is_load, kNotQuad); |
| } |
| |
| // .tp variants. |
| auto subname = |
| absl::StrCat(name_with_type, "TP", "XX", strip_mine ? "M" : ""); |
| MemoryLoadStoreOpTestHelper<T>(subname, kLength, kStride, strip_mine, |
| kPostIncrement, kNotXVariant, is_load, |
| kNotQuad); |
| } |
| } |
| |
| template <typename T> |
| void StoreQuadOpTestHelper(absl::string_view name) { |
| const auto name_with_type = absl::StrCat(name, KelvinTestTypeSuffix<T>()); |
| constexpr bool kNotLength = false; |
| constexpr bool kStride = true; |
| constexpr bool kNotXVariant = false; |
| constexpr bool kNotLoad = false; |
| constexpr bool kIsQuad = true; |
| for (auto strip_mine : {false, true}) { |
| for (auto post_increment : {false, true}) { |
| auto subname = |
| absl::StrCat(name_with_type, "S", post_increment ? "P" : "", "XX", |
| strip_mine ? "M" : ""); |
| MemoryLoadStoreOpTestHelper<T>(subname, kNotLength, kStride, strip_mine, |
| post_increment, kNotXVariant, kNotLoad, |
| kIsQuad); |
| } |
| } |
| } |
| |
| template <typename T1, typename TNext1, typename... TNext> |
| void MemoryLoadStoreOpTestHelper(absl::string_view name, bool is_load) { |
| MemoryLoadStoreOpTestHelper<T1>(name, is_load); |
| MemoryLoadStoreOpTestHelper<TNext1, TNext...>(name, is_load); |
| } |
| |
| template <typename T1, typename TNext1, typename... TNext> |
| void StoreQuadOpTestHelper(absl::string_view name) { |
| StoreQuadOpTestHelper<T1>(name); |
| StoreQuadOpTestHelper<TNext1, TNext...>(name); |
| } |
| |
| protected: |
| template <typename T> |
| T GetDefaultMemoryValue(int address) { |
| T value = 0; |
| uint8_t *ptr = reinterpret_cast<uint8_t *>(&value); |
| for (int j = 0; j < sizeof(T); j++) { |
| ptr[j] = (address + j) & 0xff; |
| } |
| return value; |
| } |
| |
| template <typename T> |
| T GetSavedMemoryValue(int address) { |
| auto *db = state_->db_factory()->Allocate<T>(1); |
| memory_->Load(address, db, nullptr, nullptr); |
| T data = db->template Get<T>(0); |
| db->DecRef(); |
| return data; |
| } |
| }; |
| |
| TEST_F(KelvinVectorMemoryInstructionsTest, VLd) { |
| MemoryLoadStoreOpTestHelper<int8_t, int16_t, int32_t>("VLd", |
| /*is_load=*/true); |
| } |
| |
| TEST_F(KelvinVectorMemoryInstructionsTest, VSt) { |
| MemoryLoadStoreOpTestHelper<int8_t, int16_t, int32_t>("VSt", |
| /*is_load=*/false); |
| } |
| |
| TEST_F(KelvinVectorMemoryInstructionsTest, VStQ) { |
| StoreQuadOpTestHelper<int8_t, int16_t, int32_t>("VStQ"); |
| } |
| |
| class KelvinGetVlInstructionTest |
| : public kelvin::sim::test::KelvinVectorInstructionsTestBase { |
| public: |
| template <typename T> |
| void GetVlTestHelper() { |
| constexpr char kRdName[] = "x8"; |
| constexpr uint32_t kMaxVlenInBytes = kelvin::sim::kVectorLengthInBits / 8; |
| auto instruction = CreateInstruction(); |
| AppendRegisterOperands( |
| instruction.get(), |
| {kelvin::sim::test::kRs1Name, kelvin::sim::test::kRs2Name}, {kRdName}); |
| for (auto strip_mine : {false, true}) { |
| for (auto is_rs1 : {false, true}) { |
| for (auto is_rs2 : {false, true}) { |
| uint32_t rs1_value = RandomValue(); |
| uint32_t rs2_value = RandomValue(); |
| SetRegisterValues<uint32_t>({{kelvin::sim::test::kRs1Name, rs1_value}, |
| {kelvin::sim::test::kRs2Name, rs2_value}, |
| {kRdName, UINT32_MAX}}); |
| instruction->set_semantic_function( |
| absl::bind_front(&KelvinGetVl<T>, strip_mine, is_rs1, is_rs2)); |
| uint32_t expected_vlen = |
| kMaxVlenInBytes / sizeof(T) * (strip_mine ? 4 : 1); |
| if (is_rs1) { |
| expected_vlen = std::min(expected_vlen, rs1_value); |
| } |
| if (is_rs2) { |
| expected_vlen = std::min(expected_vlen, rs2_value); |
| } |
| // Execute instruction. |
| instruction->Execute(nullptr); |
| EXPECT_EQ(xreg_[8]->data_buffer()->Get<uint32_t>(0), expected_vlen) |
| << "Test failed with type " |
| << (sizeof(T) == 4 ? "W" : (sizeof(T) == 2 ? "H" : "B")) |
| << ", strip_mine: " << strip_mine << ", rs1_set: " << is_rs1 |
| << ", rs2_set: " << is_rs2; |
| } |
| } |
| } |
| } |
| |
| template <typename T1, typename TNext1, typename... TNext> |
| void GetVlTestHelper() { |
| GetVlTestHelper<T1>(); |
| GetVlTestHelper<TNext1, TNext...>(); |
| } |
| |
| protected: |
| // Create a random value in the valid range for the type. |
| uint32_t RandomValue() { |
| return absl::Uniform(absl::IntervalClosed, bitgen_, |
| std::numeric_limits<uint32_t>::lowest(), |
| std::numeric_limits<uint32_t>::max()); |
| } |
| }; |
| |
| TEST_F(KelvinGetVlInstructionTest, GetVl) { |
| GetVlTestHelper<int8_t, int16_t, int32_t>(); |
| } |
| |
| class KelvinAccumulateInstructionTest |
| : public kelvin::sim::test::KelvinVectorInstructionsTestBase { |
| public: |
| void VcGetTestHelper() { |
| constexpr int kVd = 48; |
| const uint32_t kVLenInWord = state_->vector_length() / 32; |
| // Set v48..55 with random values. |
| std::vector<uint32_t> vd_value(kVLenInWord * kVLenInWord); |
| auto vd_span = absl::Span<uint32_t>(vd_value); |
| FillArrayWithRandomValues<uint32_t>(vd_span); |
| for (int i = 0; i < kVLenInWord; ++i) { |
| auto vd_name = absl::StrCat("v", kVd + i); |
| SetVectorRegisterValues<uint32_t>( |
| {{vd_name, vd_span.subspan(kVLenInWord * i, kVLenInWord)}}); |
| } |
| auto instruction = CreateInstruction(); |
| AppendVectorRegisterOperands(instruction.get(), kVLenInWord, |
| 1 /* src1_widen_factor */, {}, {}, |
| false /* widen_dst */, {kVd}); |
| instruction->set_semantic_function(&KelvinVcGet); |
| instruction->Execute(); |
| // Resulting v48..55 should all have 0 values |
| for (int i = 0; i < kVLenInWord; ++i) { |
| auto vreg_num = kVd + i; |
| auto test_vreg = vreg_[vreg_num]; |
| auto vreg_span = test_vreg->data_buffer()->Get<uint32_t>(); |
| for (int element_index = 0; element_index < kVLenInWord; |
| element_index++) { |
| EXPECT_EQ(vreg_span[element_index], 0) |
| << absl::StrCat("vreg[", vreg_num, "][", element_index, "] != 0"); |
| } |
| } |
| } |
| void AcSetTestHelper(bool is_transpose, bool expected_fail = false) { |
| constexpr int kVd = 48; |
| constexpr int kVs = 16; |
| const uint32_t kVLenInWord = state_->vector_length() / 32; |
| // Set v24..31, 48..55 with random values. |
| std::vector<uint32_t> vd_value(kVLenInWord * kVLenInWord); |
| auto vd_span = absl::Span<uint32_t>(vd_value); |
| FillArrayWithRandomValues<uint32_t>(vd_span); |
| for (int i = 0; i < kVLenInWord; ++i) { |
| auto vd_name = absl::StrCat("v", kVd + i); |
| auto vs_name = absl::StrCat("v", kVs + i); |
| SetVectorRegisterValues<uint32_t>( |
| {{vd_name, vd_span.subspan(kVLenInWord * i, kVLenInWord)}}); |
| SetVectorRegisterValues<uint32_t>( |
| {{vs_name, vd_span.subspan(kVLenInWord * i, kVLenInWord)}}); |
| } |
| auto instruction = CreateInstruction(); |
| AppendVectorRegisterOperands(instruction.get(), kVLenInWord, |
| 1 /* src1_widen_factor */, kVs, {}, |
| false /* widen_dst */, {kVd}); |
| instruction->set_semantic_function( |
| absl::bind_front(&KelvinAcSet, is_transpose)); |
| instruction->Execute(); |
| // Resulting acc_register_ should match `vs` content |
| for (int i = 0; i < kVLenInWord; ++i) { |
| auto vreg_num = kVs + i; |
| auto test_vreg = vreg_[vreg_num]; |
| auto vreg_span = test_vreg->data_buffer()->Get<uint32_t>(); |
| for (int element_index = 0; element_index < kVLenInWord; |
| element_index++) { |
| if (is_transpose) { |
| auto *acc_vec = state_->acc_vec(element_index); |
| EXPECT_EQ(vreg_span[element_index], acc_vec->at(i)) |
| << absl::StrCat("vreg[", vreg_num, "][", element_index, |
| "] != acc[", element_index, "][", i, "]"); |
| } else { |
| auto *acc_vec = state_->acc_vec(i); |
| EXPECT_EQ(vreg_span[element_index], acc_vec->at(element_index)) |
| << absl::StrCat("vreg[", vreg_num, "][", element_index, |
| "] != acc[", i, "][", element_index, "]"); |
| } |
| } |
| } |
| } |
| }; |
| |
| TEST_F(KelvinAccumulateInstructionTest, VcGet) { VcGetTestHelper(); } |
| |
| TEST_F(KelvinAccumulateInstructionTest, AcSet) { |
| AcSetTestHelper(/*is_transpose=*/false); |
| } |
| |
| TEST_F(KelvinAccumulateInstructionTest, AcTr) { |
| AcSetTestHelper(/*is_transpose=*/true); |
| } |
| |
| } // namespace |