CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
1497 lines
50 KiB
C++
1497 lines
50 KiB
C++
/* ************************************************************************
|
|
* Copyright (C) 2025-2026 Advanced Micro Devices, Inc.
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
* THE SOFTWARE.
|
|
*
|
|
* ************************************************************************ */
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <bit>
|
|
#include <cstdint>
|
|
#include <memory>
|
|
#include <sstream>
|
|
|
|
#include "stinkytofu/analysis/AnalysisRegistration.hpp"
|
|
#include "stinkytofu/core/PassManager.hpp"
|
|
#include "stinkytofu/hardware/ArchHelper.hpp"
|
|
#include "stinkytofu/ir/asm/StinkyAsmIR.hpp"
|
|
#include "stinkytofu/serialization/asm/StinkyAsmPrinter.hpp"
|
|
#include "stinkytofu/transforms/asm/BuildDefUseChain.hpp"
|
|
#include "stinkytofu/transforms/asm/PeepholeOptimizationPass.hpp"
|
|
|
|
using namespace stinkytofu;
|
|
|
|
// Test fixture for PeepholeOptimizationPass
|
|
class PeepholeOptimizationPassTest : public ::testing::Test {
|
|
protected:
|
|
GemmTileConfig gemmConfig;
|
|
GfxArchID arch;
|
|
std::unique_ptr<Function> func;
|
|
BasicBlock* bb;
|
|
std::unique_ptr<Pass> peepholePass;
|
|
AnalysisManager am;
|
|
|
|
void SetUp() override {
|
|
arch = getGfxArchID(12, 5, 0); // GFX1250
|
|
gemmConfig.arch[0] = 12;
|
|
gemmConfig.arch[1] = 5;
|
|
gemmConfig.arch[2] = 0;
|
|
|
|
// Create a Function with a single BasicBlock for testing
|
|
func = std::make_unique<Function>("test_peephole");
|
|
func->setGemmTileConfig(gemmConfig);
|
|
bb = func->createBasicBlock("entry");
|
|
|
|
// Create the peephole optimization pass
|
|
peepholePass = createPeepholeOptimizationPass();
|
|
registerAllAnalyses(am);
|
|
}
|
|
|
|
void TearDown() override {
|
|
func.reset();
|
|
bb = nullptr;
|
|
peepholePass.reset();
|
|
}
|
|
|
|
// Create IRBuilder for building test instructions
|
|
AsmIRBuilder getIRBuilder() {
|
|
return AsmIRBuilder(*bb, arch);
|
|
}
|
|
|
|
// Helper to create v_fma_f32 instruction
|
|
// v_fma_f32 dest, a, b, c => dest = a * b + c
|
|
StinkyInstruction* createVFmaF32(int destReg, int aReg, int bReg, int cReg) {
|
|
auto builder = getIRBuilder();
|
|
StinkyInstruction* inst = builder.create(getMCIDByUOp(GFX::v_fma_f32, arch));
|
|
|
|
inst->addDestReg(StinkyRegister("v", destReg, 1));
|
|
inst->addSrcReg(StinkyRegister("v", aReg, 1));
|
|
inst->addSrcReg(StinkyRegister("v", bReg, 1));
|
|
inst->addSrcReg(StinkyRegister("v", cReg, 1));
|
|
return inst;
|
|
}
|
|
|
|
// Helper to create v_fma_f32 with constant addend
|
|
// v_fma_f32 dest, a, b, #const => dest = a * b + const
|
|
StinkyInstruction* createVFmaF32WithConst(int destReg, int aReg, int bReg, float constVal) {
|
|
auto builder = getIRBuilder();
|
|
StinkyInstruction* inst = builder.create(getMCIDByUOp(GFX::v_fma_f32, arch));
|
|
|
|
inst->addDestReg(StinkyRegister("v", destReg, 1));
|
|
inst->addSrcReg(StinkyRegister("v", aReg, 1));
|
|
inst->addSrcReg(StinkyRegister("v", bReg, 1));
|
|
|
|
// Add constant operand
|
|
StinkyRegister constReg;
|
|
constReg.dataType = StinkyRegister::Type::LiteralDouble;
|
|
constReg.literalDouble = constVal;
|
|
inst->addSrcReg(constReg);
|
|
return inst;
|
|
}
|
|
|
|
// Helper to create v_fma_f16 with constant addend
|
|
StinkyInstruction* createVFmaF16WithConst(int destReg, int aReg, int bReg, float constVal) {
|
|
auto builder = getIRBuilder();
|
|
StinkyInstruction* inst = builder.create(getMCIDByUOp(GFX::v_fma_f16, arch));
|
|
|
|
inst->addDestReg(StinkyRegister("v", destReg, 1));
|
|
inst->addSrcReg(StinkyRegister("v", aReg, 1));
|
|
inst->addSrcReg(StinkyRegister("v", bReg, 1));
|
|
|
|
StinkyRegister constReg;
|
|
constReg.dataType = StinkyRegister::Type::LiteralDouble;
|
|
constReg.literalDouble = constVal;
|
|
inst->addSrcReg(constReg);
|
|
return inst;
|
|
}
|
|
|
|
// Helper to create v_add_f32 instruction
|
|
StinkyInstruction* createVAddF32(int destReg, int src0Reg, int src1Reg) {
|
|
auto builder = getIRBuilder();
|
|
StinkyInstruction* inst = builder.create(getMCIDByUOp(GFX::v_add_f32, arch));
|
|
|
|
inst->addDestReg(StinkyRegister("v", destReg, 1));
|
|
inst->addSrcReg(StinkyRegister("v", src0Reg, 1));
|
|
inst->addSrcReg(StinkyRegister("v", src1Reg, 1));
|
|
return inst;
|
|
}
|
|
|
|
// Helper to create v_add_f32 with constant
|
|
StinkyInstruction* createVAddF32WithConst(int destReg, float constVal, int srcReg) {
|
|
auto builder = getIRBuilder();
|
|
StinkyInstruction* inst = builder.create(getMCIDByUOp(GFX::v_add_f32, arch));
|
|
|
|
inst->addDestReg(StinkyRegister("v", destReg, 1));
|
|
|
|
StinkyRegister constRegister;
|
|
constRegister.dataType = StinkyRegister::Type::LiteralDouble;
|
|
constRegister.literalDouble = constVal;
|
|
inst->addSrcReg(constRegister);
|
|
inst->addSrcReg(StinkyRegister("v", srcReg, 1));
|
|
return inst;
|
|
}
|
|
|
|
// Helper to create v_add_f16 with constant
|
|
StinkyInstruction* createVAddF16WithConst(int destReg, float constVal, int srcReg) {
|
|
auto builder = getIRBuilder();
|
|
StinkyInstruction* inst = builder.create(getMCIDByUOp(GFX::v_add_f16, arch));
|
|
|
|
inst->addDestReg(StinkyRegister("v", destReg, 1));
|
|
|
|
StinkyRegister constRegister;
|
|
constRegister.dataType = StinkyRegister::Type::LiteralDouble;
|
|
constRegister.literalDouble = constVal;
|
|
inst->addSrcReg(constRegister);
|
|
inst->addSrcReg(StinkyRegister("v", srcReg, 1));
|
|
return inst;
|
|
}
|
|
|
|
// Helper to create v_add_f32 with swapped operands (register first, constant second)
|
|
// Useful for testing commutative pattern matching
|
|
StinkyInstruction* createVAddF32WithConstSwapped(int destReg, int srcReg, float constVal) {
|
|
auto builder = getIRBuilder();
|
|
StinkyInstruction* inst = builder.create(getMCIDByUOp(GFX::v_add_f32, arch));
|
|
|
|
inst->addDestReg(StinkyRegister("v", destReg, 1));
|
|
inst->addSrcReg(StinkyRegister("v", srcReg, 1));
|
|
|
|
StinkyRegister constRegister;
|
|
constRegister.dataType = StinkyRegister::Type::LiteralDouble;
|
|
constRegister.literalDouble = constVal;
|
|
inst->addSrcReg(constRegister);
|
|
return inst;
|
|
}
|
|
|
|
// Helper to create v_mul_f32 instruction
|
|
StinkyInstruction* createVMulF32(int destReg, int src0Reg, int src1Reg) {
|
|
auto builder = getIRBuilder();
|
|
StinkyInstruction* inst = builder.create(getMCIDByUOp(GFX::v_mul_f32, arch));
|
|
|
|
inst->addDestReg(StinkyRegister("v", destReg, 1));
|
|
inst->addSrcReg(StinkyRegister("v", src0Reg, 1));
|
|
inst->addSrcReg(StinkyRegister("v", src1Reg, 1));
|
|
return inst;
|
|
}
|
|
|
|
// Helper to create v_mul_f32 with constant operand
|
|
StinkyInstruction* createVMulF32WithConst(int destReg, float constVal, int srcReg) {
|
|
auto builder = getIRBuilder();
|
|
StinkyInstruction* inst = builder.create(getMCIDByUOp(GFX::v_mul_f32, arch));
|
|
|
|
inst->addDestReg(StinkyRegister("v", destReg, 1));
|
|
|
|
StinkyRegister constRegister;
|
|
constRegister.dataType = StinkyRegister::Type::LiteralDouble;
|
|
constRegister.literalDouble = constVal;
|
|
inst->addSrcReg(constRegister);
|
|
inst->addSrcReg(StinkyRegister("v", srcReg, 1));
|
|
return inst;
|
|
}
|
|
|
|
// Helper to create v_mul_f16
|
|
StinkyInstruction* createVMulF16(int destReg, int src0Reg, int src1Reg) {
|
|
auto builder = getIRBuilder();
|
|
StinkyInstruction* inst = builder.create(getMCIDByUOp(GFX::v_mul_f16, arch));
|
|
|
|
inst->addDestReg(StinkyRegister("v", destReg, 1));
|
|
inst->addSrcReg(StinkyRegister("v", src0Reg, 1));
|
|
inst->addSrcReg(StinkyRegister("v", src1Reg, 1));
|
|
return inst;
|
|
}
|
|
|
|
// Helper to create v_mul_f16 with constant operand
|
|
StinkyInstruction* createVMulF16WithConst(int destReg, float constVal, int srcReg) {
|
|
auto builder = getIRBuilder();
|
|
StinkyInstruction* inst = builder.create(getMCIDByUOp(GFX::v_mul_f16, arch));
|
|
|
|
inst->addDestReg(StinkyRegister("v", destReg, 1));
|
|
|
|
StinkyRegister constRegister;
|
|
constRegister.dataType = StinkyRegister::Type::LiteralDouble;
|
|
constRegister.literalDouble = constVal;
|
|
inst->addSrcReg(constRegister);
|
|
inst->addSrcReg(StinkyRegister("v", srcReg, 1));
|
|
return inst;
|
|
}
|
|
|
|
// Helper to create v_fma_f16 (base version)
|
|
StinkyInstruction* createVFmaF16(int destReg, int aReg, int bReg, int cReg) {
|
|
auto builder = getIRBuilder();
|
|
StinkyInstruction* inst = builder.create(getMCIDByUOp(GFX::v_fma_f16, arch));
|
|
|
|
inst->addDestReg(StinkyRegister("v", destReg, 1));
|
|
inst->addSrcReg(StinkyRegister("v", aReg, 1));
|
|
inst->addSrcReg(StinkyRegister("v", bReg, 1));
|
|
inst->addSrcReg(StinkyRegister("v", cReg, 1));
|
|
return inst;
|
|
}
|
|
|
|
// Helper to create v_mov_b32
|
|
StinkyInstruction* createVMovB32(int destReg, int srcReg) {
|
|
auto builder = getIRBuilder();
|
|
StinkyInstruction* inst = builder.create(getMCIDByUOp(GFX::v_mov_b32, arch));
|
|
|
|
inst->addDestReg(StinkyRegister("v", destReg, 1));
|
|
inst->addSrcReg(StinkyRegister("v", srcReg, 1));
|
|
return inst;
|
|
}
|
|
|
|
// Count instructions in the IRList
|
|
size_t countInstructions() {
|
|
size_t count = 0;
|
|
for (auto& inst : *bb) {
|
|
(void)inst; // Unused, just counting
|
|
count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
// Run the peephole pass
|
|
void runPass() {
|
|
PassContext ctx;
|
|
ctx.setGemmTileConfig(gemmConfig);
|
|
|
|
// Build use-def chains before running the pass (standalone test mode)
|
|
// In production, OptimizationPipeline builds this once at the start
|
|
buildUseDefChain(*func, true);
|
|
|
|
peepholePass->run(*func, ctx, am);
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// Pattern 1: Add+FMA Fusion (F32) - In-place variant
|
|
// ============================================================================
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, AddFMAFusion_F32_InPlace) {
|
|
// Create pattern:
|
|
// v_fma_f32 v0, v1, v2, 1.0 // v0 = v1 * v2 + 1.0
|
|
// v_add_f32 v0, 1.0, v0 // v0 = 1.0 + v0
|
|
// Expected after optimization:
|
|
// v_fma_f32 v0, v1, v2, 2.0 // v0 = v1 * v2 + 2.0
|
|
|
|
createVFmaF32WithConst(0, 1, 2, 1.0f);
|
|
createVAddF32WithConst(0, 1.0f, 0);
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
// After fusion, should have only 1 instruction
|
|
EXPECT_EQ(countInstructions(), 1);
|
|
|
|
// The remaining instruction should be v_fma_f32 with constant 2.0
|
|
auto& inst = *bb->begin();
|
|
auto* fma = static_cast<StinkyInstruction*>(&inst);
|
|
ASSERT_NE(fma, nullptr);
|
|
EXPECT_EQ(fma->getUnifiedOpcode(), static_cast<uint16_t>(GFX::v_fma_f32));
|
|
EXPECT_EQ(fma->getDestRegs()[0].reg.idx, 0u);
|
|
EXPECT_EQ(fma->getSrcRegs()[0].reg.idx, 1u);
|
|
EXPECT_EQ(fma->getSrcRegs()[1].reg.idx, 2u);
|
|
EXPECT_EQ(fma->getSrcRegs()[2].dataType, StinkyRegister::Type::LiteralDouble);
|
|
EXPECT_FLOAT_EQ(fma->getSrcRegs()[2].literalDouble, 2.0f);
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, AddFMAFusion_F32_NonInPlace) {
|
|
// Create pattern:
|
|
// v_fma_f32 v0, v1, v2, 1.5 // v0 = v1 * v2 + 1.5
|
|
// v_add_f32 v3, 0.5, v0 // v3 = 0.5 + v0
|
|
// Expected after optimization:
|
|
// v_fma_f32 v3, v1, v2, 2.0 // v3 = v1 * v2 + 2.0
|
|
|
|
createVFmaF32WithConst(0, 1, 2, 1.5f);
|
|
createVAddF32WithConst(3, 0.5f, 0);
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
EXPECT_EQ(countInstructions(), 1);
|
|
|
|
auto& inst = *bb->begin();
|
|
auto* fma = static_cast<StinkyInstruction*>(&inst);
|
|
ASSERT_NE(fma, nullptr);
|
|
EXPECT_EQ(fma->getUnifiedOpcode(), static_cast<uint16_t>(GFX::v_fma_f32));
|
|
EXPECT_EQ(fma->getDestRegs()[0].reg.idx, 3u); // Destination changed to v3
|
|
EXPECT_EQ(fma->getSrcRegs()[0].reg.idx, 1u);
|
|
EXPECT_EQ(fma->getSrcRegs()[1].reg.idx, 2u);
|
|
EXPECT_EQ(fma->getSrcRegs()[2].dataType, StinkyRegister::Type::LiteralDouble);
|
|
EXPECT_FLOAT_EQ(fma->getSrcRegs()[2].literalDouble, 2.0f);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Pattern 2: Add+FMA Fusion (F16)
|
|
// ============================================================================
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, AddFMAFusion_F16_InPlace) {
|
|
// Create pattern:
|
|
// v_fma_f16 v10, v11, v12, 0.5 // v10 = v11 * v12 + 0.5
|
|
// v_add_f16 v10, 0.5, v10 // v10 = 0.5 + v10
|
|
// Expected after optimization:
|
|
// v_fma_f16 v10, v11, v12, 1.0 // v10 = v11 * v12 + 1.0
|
|
|
|
createVFmaF16WithConst(10, 11, 12, 0.5f);
|
|
createVAddF16WithConst(10, 0.5f, 10);
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
EXPECT_EQ(countInstructions(), 1);
|
|
|
|
auto& inst = *bb->begin();
|
|
auto* fma = static_cast<StinkyInstruction*>(&inst);
|
|
ASSERT_NE(fma, nullptr);
|
|
EXPECT_EQ(fma->getUnifiedOpcode(), static_cast<uint16_t>(GFX::v_fma_f16));
|
|
EXPECT_EQ(fma->getDestRegs()[0].reg.idx, 10u);
|
|
EXPECT_EQ(fma->getSrcRegs()[2].dataType, StinkyRegister::Type::LiteralDouble);
|
|
EXPECT_FLOAT_EQ(fma->getSrcRegs()[2].literalDouble, 1.0f);
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, AddFMAFusion_F16_NonInPlace) {
|
|
// Create pattern:
|
|
// v_fma_f16 v5, v6, v7, 2.0 // v5 = v6 * v7 + 2.0
|
|
// v_add_f16 v8, 3.0, v5 // v8 = 3.0 + v5
|
|
// Expected after optimization:
|
|
// v_fma_f16 v8, v6, v7, 5.0 // v8 = v6 * v7 + 5.0
|
|
|
|
createVFmaF16WithConst(5, 6, 7, 2.0f);
|
|
createVAddF16WithConst(8, 3.0f, 5);
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
EXPECT_EQ(countInstructions(), 1);
|
|
|
|
auto& inst = *bb->begin();
|
|
auto* fma = static_cast<StinkyInstruction*>(&inst);
|
|
ASSERT_NE(fma, nullptr);
|
|
EXPECT_EQ(fma->getUnifiedOpcode(), static_cast<uint16_t>(GFX::v_fma_f16));
|
|
EXPECT_EQ(fma->getDestRegs()[0].reg.idx, 8u);
|
|
EXPECT_EQ(fma->getSrcRegs()[2].dataType, StinkyRegister::Type::LiteralDouble);
|
|
EXPECT_FLOAT_EQ(fma->getSrcRegs()[2].literalDouble, 5.0f);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Negative Tests: Pattern Should NOT Match
|
|
// ============================================================================
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, NoFusion_FMAResultHasMultipleUses) {
|
|
// Create pattern:
|
|
// v_fma_f32 v0, v1, v2, 1.0
|
|
// v_add_f32 v3, 1.0, v0
|
|
// v_mul_f32 v4, v0, v0 // v0 used again!
|
|
// Should NOT fuse because v0 has multiple uses
|
|
|
|
createVFmaF32WithConst(0, 1, 2, 1.0f);
|
|
createVAddF32WithConst(3, 1.0f, 0);
|
|
createVMulF32(4, 0, 0);
|
|
|
|
ASSERT_EQ(countInstructions(), 3);
|
|
|
|
runPass();
|
|
|
|
// Should still have 3 instructions
|
|
EXPECT_EQ(countInstructions(), 3);
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, NoFusion_FMAConstantMissing) {
|
|
// Create pattern:
|
|
// v_fma_f32 v0, v1, v2, v3 // Uses register v3, not constant
|
|
// v_add_f32 v0, 1.0, v0
|
|
// Should NOT fuse because FMA doesn't have constant addend
|
|
|
|
createVFmaF32(0, 1, 2, 3); // All registers, no constant
|
|
createVAddF32WithConst(0, 1.0f, 0);
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
// Should still have 2 instructions
|
|
EXPECT_EQ(countInstructions(), 2);
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, NoFusion_AddConstantMissing) {
|
|
// Create pattern:
|
|
// v_fma_f32 v0, v1, v2, 1.0
|
|
// v_add_f32 v0, v3, v0 // Uses register v3, not constant
|
|
// Should NOT fuse because ADD doesn't have constant addend
|
|
|
|
createVFmaF32WithConst(0, 1, 2, 1.0f);
|
|
createVAddF32(0, 3, 0); // All registers, no constant
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
// Should still have 2 instructions
|
|
EXPECT_EQ(countInstructions(), 2);
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, NoFusion_WrongOrder) {
|
|
// Create pattern with wrong order:
|
|
// v_add_f32 v0, 1.0, v1 // ADD comes first
|
|
// v_fma_f32 v1, v2, v3, 1.0 // FMA comes second
|
|
// Should NOT fuse because ADD comes before FMA
|
|
|
|
createVAddF32WithConst(0, 1.0f, 1);
|
|
createVFmaF32WithConst(1, 2, 3, 1.0f);
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
// Should still have 2 instructions
|
|
EXPECT_EQ(countInstructions(), 2);
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, Fusion_NonConsecutive) {
|
|
// Create pattern with intervening instruction:
|
|
// v_fma_f32 v0, v1, v2, 1.0
|
|
// v_mul_f32 v5, v6, v7 // Intervening instruction (doesn't touch v0)
|
|
// v_add_f32 v0, 1.0, v0
|
|
//
|
|
// This SHOULD fuse! The intervening v_mul doesn't affect v0, so it's safe.
|
|
// Modern peephole optimization doesn't require strict consecutiveness -
|
|
// it uses def-use analysis to determine safety.
|
|
|
|
createVFmaF32WithConst(0, 1, 2, 1.0f);
|
|
createVMulF32(5, 6, 7);
|
|
createVAddF32WithConst(0, 1.0f, 0);
|
|
|
|
ASSERT_EQ(countInstructions(), 3);
|
|
|
|
runPass();
|
|
|
|
// After fusion: v_fma_f32 + v_mul_f32 (ADD removed)
|
|
EXPECT_EQ(countInstructions(), 2);
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, NoFusion_InterveningDefOverwrites) {
|
|
// Create pattern with intervening instruction that OVERWRITES the FMA result:
|
|
// v_fma_f32 v0, v1, v2, 1.0 // Defines v0
|
|
// v_mul_f32 v0, v6, v7 // Overwrites v0!
|
|
// v_add_f32 v3, 1.0, v0 // Uses v0 (from v_mul, not v_fma!)
|
|
//
|
|
// This should NOT fuse because the ADD uses the v_mul result, not the v_fma result
|
|
|
|
createVFmaF32WithConst(0, 1, 2, 1.0f);
|
|
createVMulF32(0, 6, 7); // Overwrites v0!
|
|
createVAddF32WithConst(3, 1.0f, 0);
|
|
|
|
ASSERT_EQ(countInstructions(), 3);
|
|
|
|
runPass();
|
|
|
|
// No fusion should occur
|
|
EXPECT_EQ(countInstructions(), 3);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Edge Cases
|
|
// ============================================================================
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, EmptyIRList) {
|
|
// Empty IR list should not crash
|
|
ASSERT_EQ(countInstructions(), 0);
|
|
|
|
runPass();
|
|
|
|
EXPECT_EQ(countInstructions(), 0);
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, SingleInstruction) {
|
|
// Single instruction should not crash
|
|
createVFmaF32WithConst(0, 1, 2, 1.0f);
|
|
|
|
ASSERT_EQ(countInstructions(), 1);
|
|
|
|
runPass();
|
|
|
|
EXPECT_EQ(countInstructions(), 1);
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, NegativeConstants) {
|
|
// Test with negative constants
|
|
// v_fma_f32 v0, v1, v2, -1.0
|
|
// v_add_f32 v0, -2.0, v0
|
|
// Expected: v_fma_f32 v0, v1, v2, -3.0
|
|
|
|
createVFmaF32WithConst(0, 1, 2, -1.0f);
|
|
createVAddF32WithConst(0, -2.0f, 0);
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
EXPECT_EQ(countInstructions(), 1);
|
|
|
|
auto& inst = *bb->begin();
|
|
auto* fma = static_cast<StinkyInstruction*>(&inst);
|
|
ASSERT_NE(fma, nullptr);
|
|
EXPECT_EQ(fma->getSrcRegs()[2].dataType, StinkyRegister::Type::LiteralDouble);
|
|
EXPECT_FLOAT_EQ(fma->getSrcRegs()[2].literalDouble, -3.0f);
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, ZeroConstants) {
|
|
// Test with zero constants
|
|
// v_fma_f32 v0, v1, v2, 0.0
|
|
// v_add_f32 v0, 0.0, v0
|
|
// Expected: v_fma_f32 v0, v1, v2, 0.0
|
|
|
|
createVFmaF32WithConst(0, 1, 2, 0.0f);
|
|
createVAddF32WithConst(0, 0.0f, 0);
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
EXPECT_EQ(countInstructions(), 1);
|
|
|
|
auto& inst = *bb->begin();
|
|
auto* fma = static_cast<StinkyInstruction*>(&inst);
|
|
ASSERT_NE(fma, nullptr);
|
|
EXPECT_EQ(fma->getSrcRegs()[2].dataType, StinkyRegister::Type::LiteralDouble);
|
|
EXPECT_FLOAT_EQ(fma->getSrcRegs()[2].literalDouble, 0.0f);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Multiple Patterns in Same Block
|
|
// ============================================================================
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, MultipleFusionOpportunities) {
|
|
// Create two independent fusion opportunities:
|
|
// v_fma_f32 v0, v1, v2, 1.0
|
|
// v_add_f32 v0, 1.0, v0
|
|
// v_fma_f32 v10, v11, v12, 2.0
|
|
// v_add_f32 v10, 3.0, v10
|
|
// Expected: Both should fuse
|
|
|
|
createVFmaF32WithConst(0, 1, 2, 1.0f);
|
|
createVAddF32WithConst(0, 1.0f, 0);
|
|
createVFmaF32WithConst(10, 11, 12, 2.0f);
|
|
createVAddF32WithConst(10, 3.0f, 10);
|
|
|
|
ASSERT_EQ(countInstructions(), 4);
|
|
|
|
runPass();
|
|
|
|
// Should have 2 instructions after both fusions
|
|
EXPECT_EQ(countInstructions(), 2);
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, MixedF32AndF16) {
|
|
// Create one F32 and one F16 fusion:
|
|
// v_fma_f32 v0, v1, v2, 1.0
|
|
// v_add_f32 v0, 1.0, v0
|
|
// v_fma_f16 v10, v11, v12, 0.5
|
|
// v_add_f16 v10, 0.5, v10
|
|
// Expected: Both should fuse
|
|
|
|
createVFmaF32WithConst(0, 1, 2, 1.0f);
|
|
createVAddF32WithConst(0, 1.0f, 0);
|
|
createVFmaF16WithConst(10, 11, 12, 0.5f);
|
|
createVAddF16WithConst(10, 0.5f, 10);
|
|
|
|
ASSERT_EQ(countInstructions(), 4);
|
|
|
|
runPass();
|
|
|
|
EXPECT_EQ(countInstructions(), 2);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Register Reuse Tests
|
|
// ============================================================================
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, Fusion_WithRegisterReuse) {
|
|
// Test the fix for def-use analysis with register reuse (GitHub issue #XXX)
|
|
//
|
|
// This tests the scenario from GELU where the same register is redefined
|
|
// multiple times:
|
|
// v_fma_f32 v10, v1, v2, 1.0 // def #1 at pos 0
|
|
// v_add_f32 v10, 1.0, v10 // def #2 at pos 1, uses def #1
|
|
// v_mul_f32 v10, v3, v10 // def #3 at pos 2, uses def #2 (NOT def #1!)
|
|
// v_mul_f32 v5, v4, v10 // uses def #3 (NOT def #1!)
|
|
//
|
|
// The def-use analysis must recognize that def #1 has only ONE use (at pos 1),
|
|
// not THREE uses. Instructions at pos 2 and 3 use later definitions of v10.
|
|
//
|
|
// Expected: FMA+ADD fusion should succeed because def #1 has exactly one use.
|
|
|
|
createVFmaF32WithConst(10, 1, 2, 1.0f); // pos 0: v10 = v1*v2 + 1.0
|
|
createVAddF32WithConst(10, 1.0f, 10); // pos 1: v10 = 1.0 + v10 (uses pos 0's v10)
|
|
createVMulF32(10, 3, 10); // pos 2: v10 = v3 * v10 (uses pos 1's v10, redefines)
|
|
createVMulF32(5, 4, 10); // pos 3: v5 = v4 * v10 (uses pos 2's v10)
|
|
|
|
ASSERT_EQ(countInstructions(), 4);
|
|
|
|
runPass();
|
|
|
|
// After fusion: FMA+ADD should fuse into one instruction
|
|
// Remaining: fused FMA (pos 0), v_mul (pos 2), v_mul (pos 3) = 3 instructions
|
|
EXPECT_EQ(countInstructions(), 3);
|
|
|
|
// Verify the fused instruction has the correct constant
|
|
auto it = bb->begin();
|
|
auto* fma = static_cast<StinkyInstruction*>(&*it);
|
|
EXPECT_EQ(fma->getUnifiedOpcode(), static_cast<uint16_t>(GFX::v_fma_f32));
|
|
EXPECT_EQ(fma->getDestRegs()[0].reg.idx, 10u);
|
|
EXPECT_FLOAT_EQ(fma->getSrcRegs()[2].literalDouble, 2.0f); // 1.0 + 1.0 = 2.0
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, NoFusion_RegisterReuseWithMultipleUsesBeforeRedef) {
|
|
// Test that fusion DOESN'T happen when the definition truly has multiple uses
|
|
// before being redefined:
|
|
// v_fma_f32 v10, v1, v2, 1.0 // def #1
|
|
// v_mul_f32 v5, v10, v10 // use #1 of def #1
|
|
// v_add_f32 v6, 1.0, v10 // use #2 of def #1 (multiple uses!)
|
|
// v_mul_f32 v10, v3, v4 // redefines v10
|
|
//
|
|
// The FMA result has TWO uses before being redefined, so fusion should NOT happen.
|
|
|
|
createVFmaF32WithConst(10, 1, 2, 1.0f); // def v10
|
|
createVMulF32(5, 10, 10); // use v10 twice
|
|
createVAddF32WithConst(6, 1.0f, 10); // use v10 again (3rd use total)
|
|
createVMulF32(10, 3, 4); // redefine v10
|
|
|
|
ASSERT_EQ(countInstructions(), 4);
|
|
|
|
runPass();
|
|
|
|
// No fusion should occur - v10 from FMA has 3 uses before redefinition
|
|
EXPECT_EQ(countInstructions(), 4);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Integration Test with PassManager
|
|
// ============================================================================
|
|
|
|
class PeepholePassManagerTest : public ::testing::Test, public stinkytofu::PassManager {
|
|
protected:
|
|
stinkytofu::Function func{"kernel"};
|
|
GemmTileConfig gemmConfig;
|
|
GfxArchID arch;
|
|
BasicBlock* bb;
|
|
|
|
void SetUp() override {
|
|
arch = getGfxArchID(12, 5, 0); // GFX1250
|
|
gemmConfig.arch[0] = 12;
|
|
gemmConfig.arch[1] = 5;
|
|
gemmConfig.arch[2] = 0;
|
|
|
|
func.setGemmTileConfig(gemmConfig);
|
|
bb = func.createBasicBlock("entry");
|
|
|
|
setGemmTileConfig(gemmConfig);
|
|
registerAllAnalyses(getAnalysisManager());
|
|
}
|
|
|
|
void TearDown() override {
|
|
bb = nullptr;
|
|
}
|
|
|
|
AsmIRBuilder getIRBuilder() {
|
|
return AsmIRBuilder(*bb, arch);
|
|
}
|
|
|
|
StinkyInstruction* createVFmaF32WithConst(int destReg, int aReg, int bReg, float constVal) {
|
|
auto builder = getIRBuilder();
|
|
StinkyInstruction* inst = builder.create(getMCIDByUOp(GFX::v_fma_f32, arch));
|
|
|
|
inst->addDestReg(StinkyRegister("v", destReg, 1));
|
|
inst->addSrcReg(StinkyRegister("v", aReg, 1));
|
|
inst->addSrcReg(StinkyRegister("v", bReg, 1));
|
|
|
|
StinkyRegister constReg;
|
|
constReg.dataType = StinkyRegister::Type::LiteralDouble;
|
|
constReg.literalDouble = constVal;
|
|
inst->addSrcReg(constReg);
|
|
return inst;
|
|
}
|
|
|
|
StinkyInstruction* createVAddF32WithConst(int destReg, float constVal, int srcReg) {
|
|
auto builder = getIRBuilder();
|
|
StinkyInstruction* inst = builder.create(getMCIDByUOp(GFX::v_add_f32, arch));
|
|
|
|
inst->addDestReg(StinkyRegister("v", destReg, 1));
|
|
|
|
StinkyRegister constRegister;
|
|
constRegister.dataType = StinkyRegister::Type::LiteralDouble;
|
|
constRegister.literalDouble = constVal;
|
|
inst->addSrcReg(constRegister);
|
|
inst->addSrcReg(StinkyRegister("v", srcReg, 1));
|
|
return inst;
|
|
}
|
|
|
|
size_t countInstructions() {
|
|
size_t count = 0;
|
|
for (auto& inst : *bb) {
|
|
(void)inst;
|
|
count++;
|
|
}
|
|
return count;
|
|
}
|
|
};
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
// Test: Commutative Pattern Matching
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, Fusion_CommutativeOperandOrder) {
|
|
// Test that v_add_f32 fusion works when operands are swapped
|
|
// Pattern expects: v_add_f32 $dst, $const, $reg
|
|
// This test has: v_add_f32 $dst, $reg, $const (swapped!)
|
|
// Should still match because v_add_f32 is commutative
|
|
|
|
createVFmaF32WithConst(10, 1, 2, 1.0f); // v10 = v1*v2 + 1.0
|
|
createVAddF32WithConstSwapped(11, 10, 1.0f); // v11 = v10 + 1.0 (operands swapped!)
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
// Should fuse: v_fma_f32 v11, v1, v2, 2.0
|
|
EXPECT_EQ(countInstructions(), 1);
|
|
|
|
// Verify the FMA has the correct folded constant
|
|
auto& irList = *bb;
|
|
auto it = irList.begin();
|
|
ASSERT_NE(it, irList.end());
|
|
|
|
StinkyInstruction* fma = cast<StinkyInstruction>(&(*it));
|
|
EXPECT_EQ(fma->getUnifiedOpcode(), static_cast<uint16_t>(GFX::v_fma_f32));
|
|
|
|
// Check destination
|
|
ASSERT_EQ(fma->getDestRegs().size(), 1);
|
|
EXPECT_EQ(fma->getDestRegs()[0].reg.idx, 11u);
|
|
|
|
// Check constant is folded: 1.0 + 1.0 = 2.0
|
|
ASSERT_EQ(fma->getSrcRegs().size(), 3);
|
|
EXPECT_TRUE(fma->getSrcRegs()[2].dataType == StinkyRegister::Type::LiteralDouble);
|
|
EXPECT_DOUBLE_EQ(fma->getSrcRegs()[2].literalDouble, 2.0);
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, Fusion_CommutativeInPlace) {
|
|
// Test commutative matching with in-place operation
|
|
// v_fma_f32 v10, v1, v2, 1.0
|
|
// v_add_f32 v10, v10, 1.0 (register first, constant second - swapped!)
|
|
|
|
createVFmaF32WithConst(10, 1, 2, 1.0f); // v10 = v1*v2 + 1.0
|
|
createVAddF32WithConstSwapped(10, 10, 1.0f); // v10 = v10 + 1.0 (swapped, in-place!)
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
// Should fuse: v_fma_f32 v10, v1, v2, 2.0
|
|
EXPECT_EQ(countInstructions(), 1);
|
|
|
|
auto& irList = *bb;
|
|
auto it = irList.begin();
|
|
ASSERT_NE(it, irList.end());
|
|
|
|
StinkyInstruction* fma = cast<StinkyInstruction>(&(*it));
|
|
EXPECT_EQ(fma->getUnifiedOpcode(), static_cast<uint16_t>(GFX::v_fma_f32));
|
|
|
|
// Check destination is v10
|
|
ASSERT_EQ(fma->getDestRegs().size(), 1);
|
|
EXPECT_EQ(fma->getDestRegs()[0].reg.idx, 10u);
|
|
|
|
// Check constant is folded
|
|
ASSERT_EQ(fma->getSrcRegs().size(), 3);
|
|
EXPECT_DOUBLE_EQ(fma->getSrcRegs()[2].literalDouble, 2.0);
|
|
}
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
// Test: MUL+MUL Fusion
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, MULMULFusion_F32_Basic) {
|
|
// Test basic MUL+MUL fusion with constant folding
|
|
// v_mul_f32 v0, 2.0, v1 // v0 = 2.0 * v1
|
|
// v_mul_f32 v2, 3.0, v0 // v2 = 3.0 * v0
|
|
// => v_mul_f32 v2, 6.0, v1 // v2 = 6.0 * v1 (folded: 2.0 * 3.0 = 6.0)
|
|
|
|
createVMulF32WithConst(0, 2.0f, 1); // v0 = 2.0 * v1
|
|
createVMulF32WithConst(2, 3.0f, 0); // v2 = 3.0 * v0
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
// After fusion, should have only 1 instruction
|
|
EXPECT_EQ(countInstructions(), 1);
|
|
|
|
// Verify the result: v_mul_f32 v2, 6.0, v1
|
|
auto& inst = *bb->begin();
|
|
auto* mul = static_cast<StinkyInstruction*>(&inst);
|
|
ASSERT_NE(mul, nullptr);
|
|
EXPECT_EQ(mul->getUnifiedOpcode(), static_cast<uint16_t>(GFX::v_mul_f32));
|
|
EXPECT_EQ(mul->getDestRegs()[0].reg.idx, 2u); // Destination is v2
|
|
EXPECT_EQ(mul->getSrcRegs()[1].reg.idx, 1u); // Source is v1
|
|
EXPECT_EQ(mul->getSrcRegs()[0].dataType, StinkyRegister::Type::LiteralDouble);
|
|
EXPECT_FLOAT_EQ(mul->getSrcRegs()[0].literalDouble, 6.0f); // 2.0 * 3.0 = 6.0
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, MULMULFusion_F32_InPlace) {
|
|
// Test in-place MUL+MUL fusion
|
|
// v_mul_f32 v0, 2.0, v1 // v0 = 2.0 * v1
|
|
// v_mul_f32 v0, 3.0, v0 // v0 = 3.0 * v0 (in-place)
|
|
// => v_mul_f32 v0, 6.0, v1 // v0 = 6.0 * v1
|
|
|
|
createVMulF32WithConst(0, 2.0f, 1); // v0 = 2.0 * v1
|
|
createVMulF32WithConst(0, 3.0f, 0); // v0 = 3.0 * v0 (in-place)
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
EXPECT_EQ(countInstructions(), 1);
|
|
|
|
auto& inst = *bb->begin();
|
|
auto* mul = static_cast<StinkyInstruction*>(&inst);
|
|
EXPECT_EQ(mul->getUnifiedOpcode(), static_cast<uint16_t>(GFX::v_mul_f32));
|
|
EXPECT_EQ(mul->getDestRegs()[0].reg.idx, 0u); // Destination is v0
|
|
EXPECT_EQ(mul->getSrcRegs()[1].reg.idx, 1u); // Source is v1
|
|
EXPECT_FLOAT_EQ(mul->getSrcRegs()[0].literalDouble, 6.0f);
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, MULMULFusion_F16_Basic) {
|
|
// Test MUL+MUL fusion for F16
|
|
// v_mul_f16 v0, 2.5, v1 // v0 = 2.5 * v1
|
|
// v_mul_f16 v2, 4.0, v0 // v2 = 4.0 * v0
|
|
// => v_mul_f16 v2, 10.0, v1 // v2 = 10.0 * v1 (folded: 2.5 * 4.0 = 10.0)
|
|
|
|
createVMulF16WithConst(0, 2.5f, 1); // v0 = 2.5 * v1
|
|
createVMulF16WithConst(2, 4.0f, 0); // v2 = 4.0 * v0
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
EXPECT_EQ(countInstructions(), 1);
|
|
|
|
auto& inst = *bb->begin();
|
|
auto* mul = static_cast<StinkyInstruction*>(&inst);
|
|
EXPECT_EQ(mul->getUnifiedOpcode(), static_cast<uint16_t>(GFX::v_mul_f16));
|
|
EXPECT_EQ(mul->getDestRegs()[0].reg.idx, 2u);
|
|
EXPECT_EQ(mul->getSrcRegs()[1].reg.idx, 1u);
|
|
EXPECT_FLOAT_EQ(mul->getSrcRegs()[0].literalDouble, 10.0f); // 2.5 * 4.0 = 10.0
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, NoFusion_MUL_MultipleUses) {
|
|
// First MUL result has multiple uses - should NOT fuse
|
|
// v_mul_f32 v0, 2.0, v1 // v0 = 2.0 * v1
|
|
// v_mul_f32 v2, 3.0, v0 // v2 = 3.0 * v0
|
|
// v_mul_f32 v3, 4.0, v0 // v3 = 4.0 * v0 (v0 used again!)
|
|
|
|
createVMulF32WithConst(0, 2.0f, 1); // v0 = 2.0 * v1
|
|
createVMulF32WithConst(2, 3.0f, 0); // v2 = 3.0 * v0
|
|
createVMulF32WithConst(3, 4.0f, 0); // v3 = 4.0 * v0 (v0 has 2 uses)
|
|
|
|
ASSERT_EQ(countInstructions(), 3);
|
|
|
|
runPass();
|
|
|
|
// Should NOT fuse because v0 has multiple uses
|
|
EXPECT_EQ(countInstructions(), 3);
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, NoFusion_MUL_MissingConstant) {
|
|
// Second MUL doesn't have a constant - should NOT fuse
|
|
// v_mul_f32 v0, 2.0, v1 // v0 = 2.0 * v1
|
|
// v_mul_f32 v2, v3, v0 // v2 = v3 * v0 (no constant!)
|
|
|
|
createVMulF32WithConst(0, 2.0f, 1); // v0 = 2.0 * v1
|
|
createVMulF32(2, 3, 0); // v2 = v3 * v0 (no constant)
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
// Should NOT fuse because second MUL doesn't have constant
|
|
EXPECT_EQ(countInstructions(), 2);
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, MULMULFusion_WithZero) {
|
|
// Test constant folding with zero
|
|
// v_mul_f32 v0, 5.0, v1 // v0 = 5.0 * v1
|
|
// v_mul_f32 v2, 0.0, v0 // v2 = 0.0 * v0
|
|
// => v_mul_f32 v2, 0.0, v1 // v2 = 0.0 * v1 (folded: 5.0 * 0.0 = 0.0)
|
|
|
|
createVMulF32WithConst(0, 5.0f, 1); // v0 = 5.0 * v1
|
|
createVMulF32WithConst(2, 0.0f, 0); // v2 = 0.0 * v0
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
EXPECT_EQ(countInstructions(), 1);
|
|
|
|
auto& inst = *bb->begin();
|
|
auto* mul = static_cast<StinkyInstruction*>(&inst);
|
|
EXPECT_FLOAT_EQ(mul->getSrcRegs()[0].literalDouble, 0.0f); // 5.0 * 0.0 = 0.0
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, MULMULFusion_Negative) {
|
|
// Test constant folding with negative values
|
|
// v_mul_f32 v0, -2.0, v1 // v0 = -2.0 * v1
|
|
// v_mul_f32 v2, 3.0, v0 // v2 = 3.0 * v0
|
|
// => v_mul_f32 v2, -6.0, v1 // v2 = -6.0 * v1 (folded: -2.0 * 3.0 = -6.0)
|
|
|
|
createVMulF32WithConst(0, -2.0f, 1); // v0 = -2.0 * v1
|
|
createVMulF32WithConst(2, 3.0f, 0); // v2 = 3.0 * v0
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
EXPECT_EQ(countInstructions(), 1);
|
|
|
|
auto& inst = *bb->begin();
|
|
auto* mul = static_cast<StinkyInstruction*>(&inst);
|
|
EXPECT_FLOAT_EQ(mul->getSrcRegs()[0].literalDouble, -6.0f); // -2.0 * 3.0 = -6.0
|
|
}
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
// Test: MUL+FMA Fusion
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, MULFMAFusion_F32_Basic) {
|
|
// Pattern: v_mul followed by v_fma where MUL result is used in FMA
|
|
// v_mul_f32 v0, 2.0, v1 // v0 = 2.0 * v1
|
|
// v_fma_f32 v2, 3.0, v0, v3 // v2 = 3.0 * v0 + v3 = 3.0 * (2.0 * v1) + v3
|
|
// => v_fma_f32 v2, 6.0, v1, v3 // v2 = 6.0 * v1 + v3 (folded: 2.0 * 3.0 = 6.0)
|
|
|
|
createVMulF32WithConst(0, 2.0f, 1); // v0 = 2.0 * v1
|
|
|
|
// Create v_fma_f32 v2, 3.0, v0, v3
|
|
auto* fma = createVFmaF32(2, 10, 0, 3); // Base FMA: v2 = v10 * v0 + v3
|
|
{
|
|
auto regs = fma->getSrcRegs();
|
|
regs[0] = StinkyRegister(3.0);
|
|
fma->setSrcRegs(regs);
|
|
} // Change to: v2 = 3.0 * v0 + v3
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
// Should fuse into single FMA with folded constant
|
|
EXPECT_EQ(countInstructions(), 1);
|
|
|
|
auto& irList = *bb;
|
|
auto it = irList.begin();
|
|
ASSERT_NE(it, irList.end());
|
|
|
|
StinkyInstruction* fma_result = cast<StinkyInstruction>(&(*it));
|
|
EXPECT_EQ(fma_result->getUnifiedOpcode(), static_cast<uint16_t>(GFX::v_fma_f32));
|
|
|
|
// Check destination is v2
|
|
ASSERT_EQ(fma_result->getDestRegs().size(), 1);
|
|
EXPECT_EQ(fma_result->getDestRegs()[0].reg.idx, 2u);
|
|
|
|
// Check constant is folded: 2.0 * 3.0 = 6.0
|
|
ASSERT_EQ(fma_result->getSrcRegs().size(), 3);
|
|
EXPECT_TRUE(fma_result->getSrcRegs()[0].dataType == StinkyRegister::Type::LiteralDouble);
|
|
EXPECT_FLOAT_EQ(fma_result->getSrcRegs()[0].literalDouble, 6.0f);
|
|
|
|
// Check that v1 is the operand (from original MUL)
|
|
EXPECT_EQ(fma_result->getSrcRegs()[1].reg.idx, 1u);
|
|
|
|
// Check that v3 is preserved as the addend
|
|
EXPECT_EQ(fma_result->getSrcRegs()[2].reg.idx, 3u);
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, MULFMAFusion_F32_Commutative_MulOperand) {
|
|
// Test commutative matching: MUL result used in different position of FMA multiply
|
|
// v_mul_f32 v0, 2.0, v1 // v0 = 2.0 * v1
|
|
// v_fma_f32 v2, v0, 3.0, v3 // v2 = v0 * 3.0 + v3 (MUL result in first position!)
|
|
// Should still match because FMA multiply operands are commutative
|
|
|
|
createVMulF32WithConst(0, 2.0f, 1); // v0 = 2.0 * v1
|
|
|
|
// Create v_fma_f32 v2, v0, 3.0, v3
|
|
auto* fma = createVFmaF32(2, 0, 10, 3); // v2 = v0 * v10 + v3
|
|
{
|
|
auto regs = fma->getSrcRegs();
|
|
regs[1] = StinkyRegister(3.0);
|
|
fma->setSrcRegs(regs);
|
|
} // Change to: v2 = v0 * 3.0 + v3
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
// Should fuse
|
|
EXPECT_EQ(countInstructions(), 1);
|
|
|
|
auto& inst = *bb->begin();
|
|
auto* fma_result = static_cast<StinkyInstruction*>(&inst);
|
|
|
|
// Check constant is folded: 2.0 * 3.0 = 6.0
|
|
EXPECT_FLOAT_EQ(fma_result->getSrcRegs()[0].literalDouble, 6.0f);
|
|
}
|
|
|
|
// NOTE: Commutative matching only works for the ROOT instruction being matched,
|
|
// not for producer instructions found via def-use chains. This matches LLVM behavior.
|
|
// Test removed: MULFMAFusion_F32_Commutative_MulConstant
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, MULFMAFusion_F16_Basic) {
|
|
// Test F16 variant
|
|
// v_mul_f16 v0, 2.0, v1
|
|
// v_fma_f16 v2, 3.0, v0, v3
|
|
// => v_fma_f16 v2, 6.0, v1, v3
|
|
|
|
createVMulF16WithConst(0, 2.0f, 1); // v0 = 2.0 * v1
|
|
|
|
auto* fma = createVFmaF16(2, 10, 0, 3); // v2 = v10 * v0 + v3
|
|
{
|
|
auto regs = fma->getSrcRegs();
|
|
regs[0] = StinkyRegister(3.0);
|
|
fma->setSrcRegs(regs);
|
|
} // Change: v2 = 3.0 * v0 + v3
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
EXPECT_EQ(countInstructions(), 1);
|
|
|
|
auto& inst = *bb->begin();
|
|
auto* fma_result = static_cast<StinkyInstruction*>(&inst);
|
|
|
|
EXPECT_EQ(fma_result->getUnifiedOpcode(), static_cast<uint16_t>(GFX::v_fma_f16));
|
|
EXPECT_FLOAT_EQ(fma_result->getSrcRegs()[0].literalDouble, 6.0f);
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, NoFusion_MULFMA_MultipleUses) {
|
|
// MUL result has multiple uses - should NOT fuse
|
|
// v_mul_f32 v0, 2.0, v1
|
|
// v_fma_f32 v2, 3.0, v0, v3
|
|
// v_add_f32 v4, v0, v5 // v0 used again!
|
|
|
|
createVMulF32WithConst(0, 2.0f, 1);
|
|
|
|
auto* fma = createVFmaF32(2, 10, 0, 3);
|
|
{
|
|
auto regs = fma->getSrcRegs();
|
|
regs[0] = StinkyRegister(3.0);
|
|
fma->setSrcRegs(regs);
|
|
}
|
|
|
|
// Extra use of v0
|
|
createVAddF32(4, 0, 5);
|
|
|
|
ASSERT_EQ(countInstructions(), 3);
|
|
|
|
runPass();
|
|
|
|
// Should NOT fuse - still 3 instructions
|
|
EXPECT_EQ(countInstructions(), 3);
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, NoFusion_MULFMA_MissingConstant) {
|
|
// MUL doesn't have constant - should NOT fuse
|
|
// v_mul_f32 v0, v1, v2 // No constant!
|
|
// v_fma_f32 v3, 3.0, v0, v4
|
|
|
|
createVMulF32(0, 1, 2); // v0 = v1 * v2 (no constant)
|
|
|
|
auto* fma = createVFmaF32(3, 10, 0, 4);
|
|
{
|
|
auto regs = fma->getSrcRegs();
|
|
regs[0] = StinkyRegister(3.0);
|
|
fma->setSrcRegs(regs);
|
|
}
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
// Should NOT fuse
|
|
EXPECT_EQ(countInstructions(), 2);
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, MULFMAFusion_WithNegative) {
|
|
// Test with negative constants
|
|
// v_mul_f32 v0, -2.0, v1
|
|
// v_fma_f32 v2, 3.0, v0, v3
|
|
// => v_fma_f32 v2, -6.0, v1, v3
|
|
|
|
createVMulF32WithConst(0, -2.0f, 1);
|
|
|
|
auto* fma = createVFmaF32(2, 10, 0, 3);
|
|
{
|
|
auto regs = fma->getSrcRegs();
|
|
regs[0] = StinkyRegister(3.0);
|
|
fma->setSrcRegs(regs);
|
|
}
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
EXPECT_EQ(countInstructions(), 1);
|
|
|
|
auto& inst = *bb->begin();
|
|
auto* fma_result = static_cast<StinkyInstruction*>(&inst);
|
|
|
|
EXPECT_FLOAT_EQ(fma_result->getSrcRegs()[0].literalDouble, -6.0f);
|
|
}
|
|
|
|
// ============================================================================
|
|
// ADD+MUL Fusion Tests
|
|
// ============================================================================
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, ADDMULFusion_F32_Basic) {
|
|
// Basic ADD+MUL fusion with constant folding
|
|
// v_add_f32 v0, 2.0, v1 // v0 = 2.0 + v1
|
|
// v_mul_f32 v2, 3.0, v0 // v2 = 3.0 * v0
|
|
// => v_fma_f32 v2, 3.0, v1, 6.0 // v2 = 3.0 * v1 + 6.0
|
|
// (const2 * const1 = 3.0 * 2.0 = 6.0)
|
|
|
|
createVAddF32WithConst(0, 2.0f, 1);
|
|
createVMulF32WithConst(2, 3.0f, 0);
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
// After fusion, should have only 1 instruction
|
|
EXPECT_EQ(countInstructions(), 1);
|
|
|
|
// The remaining instruction should be v_fma_f32 with constant 6.0
|
|
auto& inst = *bb->begin();
|
|
auto* fma_result = static_cast<StinkyInstruction*>(&inst);
|
|
ASSERT_NE(fma_result, nullptr);
|
|
|
|
// Check that constants were folded: 3.0 * 2.0 = 6.0
|
|
// srcRegs[0] should be 3.0 (mul_const)
|
|
// srcRegs[2] should be 6.0 (mul_const * add_const)
|
|
EXPECT_FLOAT_EQ(fma_result->getSrcRegs()[0].literalDouble, 3.0f);
|
|
EXPECT_FLOAT_EQ(fma_result->getSrcRegs()[2].literalDouble, 6.0f);
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, ADDMULFusion_F16_Basic) {
|
|
// F16 variant of ADD+MUL fusion
|
|
// v_add_f16 v0, 2.0, v1
|
|
// v_mul_f16 v2, 3.0, v0
|
|
// => v_fma_f16 v2, 3.0, v1, 6.0
|
|
|
|
createVAddF16WithConst(0, 2.0f, 1);
|
|
createVMulF16WithConst(2, 3.0f, 0);
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
EXPECT_EQ(countInstructions(), 1);
|
|
|
|
auto& inst = *bb->begin();
|
|
auto* fma_result = static_cast<StinkyInstruction*>(&inst);
|
|
|
|
EXPECT_FLOAT_EQ(fma_result->getSrcRegs()[0].literalDouble, 3.0f);
|
|
EXPECT_FLOAT_EQ(fma_result->getSrcRegs()[2].literalDouble, 6.0f);
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, NoFusion_ADDMUL_MultipleUses) {
|
|
// ADD result has multiple uses - should NOT fuse
|
|
// v_add_f32 v0, 2.0, v1
|
|
// v_mul_f32 v2, 3.0, v0
|
|
// v_add_f32 v3, v0, v4 // v0 used again!
|
|
|
|
createVAddF32WithConst(0, 2.0f, 1);
|
|
createVMulF32WithConst(2, 3.0f, 0);
|
|
// Extra use of v0
|
|
createVAddF32(3, 0, 4);
|
|
|
|
ASSERT_EQ(countInstructions(), 3);
|
|
|
|
runPass();
|
|
|
|
// Should NOT fuse - still 3 instructions
|
|
EXPECT_EQ(countInstructions(), 3);
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, NoFusion_ADDMUL_MissingConstant_Add) {
|
|
// ADD doesn't have constant - should NOT fuse
|
|
// v_add_f32 v0, v1, v2 // No constant!
|
|
// v_mul_f32 v3, 3.0, v0
|
|
|
|
createVAddF32(0, 1, 2); // v0 = v1 + v2 (no constant)
|
|
createVMulF32WithConst(3, 3.0f, 0);
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
// Should NOT fuse
|
|
EXPECT_EQ(countInstructions(), 2);
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, NoFusion_ADDMUL_MissingConstant_Mul) {
|
|
// MUL doesn't have constant - should NOT fuse
|
|
// v_add_f32 v0, 2.0, v1
|
|
// v_mul_f32 v2, v3, v0 // No constant!
|
|
|
|
createVAddF32WithConst(0, 2.0f, 1);
|
|
createVMulF32(2, 3, 0); // v2 = v3 * v0 (no constant)
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
// Should NOT fuse
|
|
EXPECT_EQ(countInstructions(), 2);
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, ADDMULFusion_WithNegative) {
|
|
// Test with negative constants
|
|
// v_add_f32 v0, -2.0, v1
|
|
// v_mul_f32 v2, 3.0, v0
|
|
// => v_fma_f32 v2, 3.0, v1, -6.0
|
|
// (3.0 * -2.0 = -6.0)
|
|
|
|
createVAddF32WithConst(0, -2.0f, 1);
|
|
createVMulF32WithConst(2, 3.0f, 0);
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
EXPECT_EQ(countInstructions(), 1);
|
|
|
|
auto& inst = *bb->begin();
|
|
auto* fma_result = static_cast<StinkyInstruction*>(&inst);
|
|
|
|
EXPECT_FLOAT_EQ(fma_result->getSrcRegs()[0].literalDouble, 3.0f);
|
|
EXPECT_FLOAT_EQ(fma_result->getSrcRegs()[2].literalDouble, -6.0f);
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, ADDMULFusion_BothNegative) {
|
|
// Test with both constants negative
|
|
// v_add_f32 v0, -2.0, v1
|
|
// v_mul_f32 v2, -3.0, v0
|
|
// => v_fma_f32 v2, -3.0, v1, 6.0
|
|
// (-3.0 * -2.0 = 6.0)
|
|
|
|
createVAddF32WithConst(0, -2.0f, 1);
|
|
createVMulF32WithConst(2, -3.0f, 0);
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
EXPECT_EQ(countInstructions(), 1);
|
|
|
|
auto& inst = *bb->begin();
|
|
auto* fma_result = static_cast<StinkyInstruction*>(&inst);
|
|
|
|
EXPECT_FLOAT_EQ(fma_result->getSrcRegs()[0].literalDouble, -3.0f);
|
|
EXPECT_FLOAT_EQ(fma_result->getSrcRegs()[2].literalDouble, 6.0f);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Move Propagation Tests
|
|
// ============================================================================
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, MovPropagation_F32_Add_Basic) {
|
|
// v_mov_b32 v1, v0
|
|
// v_add_f32 v2, 2.0, v1
|
|
// => v_add_f32 v2, 2.0, v0
|
|
|
|
createVMovB32(1, 0);
|
|
createVAddF32WithConst(2, 2.0f, 1);
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
EXPECT_EQ(countInstructions(), 1);
|
|
|
|
auto& inst = *bb->begin();
|
|
auto* add = static_cast<StinkyInstruction*>(&inst);
|
|
EXPECT_EQ(add->getUnifiedOpcode(), static_cast<uint16_t>(GFX::v_add_f32));
|
|
EXPECT_EQ(add->getDestRegs()[0].reg.idx, 2u);
|
|
EXPECT_EQ(add->getSrcRegs()[1].reg.idx, 0u); // Should use v0, not v1
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, MovPropagation_F16_Add_Basic) {
|
|
// v_mov_b32 v1, v0
|
|
// v_add_f16 v2, 2.0, v1
|
|
// => v_add_f16 v2, 2.0, v0
|
|
|
|
createVMovB32(1, 0);
|
|
createVAddF16WithConst(2, 2.0f, 1);
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
EXPECT_EQ(countInstructions(), 1);
|
|
|
|
auto& inst = *bb->begin();
|
|
auto* add = static_cast<StinkyInstruction*>(&inst);
|
|
EXPECT_EQ(add->getUnifiedOpcode(), static_cast<uint16_t>(GFX::v_add_f16));
|
|
EXPECT_EQ(add->getDestRegs()[0].reg.idx, 2u);
|
|
EXPECT_EQ(add->getSrcRegs()[1].reg.idx, 0u);
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, MovPropagation_F32_Mul_Basic) {
|
|
// v_mov_b32 v1, v0
|
|
// v_mul_f32 v2, 2.0, v1
|
|
// => v_mul_f32 v2, 2.0, v0
|
|
|
|
createVMovB32(1, 0);
|
|
createVMulF32WithConst(2, 2.0f, 1);
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
EXPECT_EQ(countInstructions(), 1);
|
|
|
|
auto& inst = *bb->begin();
|
|
auto* mul = static_cast<StinkyInstruction*>(&inst);
|
|
EXPECT_EQ(mul->getUnifiedOpcode(), static_cast<uint16_t>(GFX::v_mul_f32));
|
|
EXPECT_EQ(mul->getDestRegs()[0].reg.idx, 2u);
|
|
EXPECT_EQ(mul->getSrcRegs()[1].reg.idx, 0u);
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, MovPropagation_F16_Mul_Basic) {
|
|
// v_mov_b32 v1, v0
|
|
// v_mul_f16 v2, 2.0, v1
|
|
// => v_mul_f16 v2, 2.0, v0
|
|
|
|
createVMovB32(1, 0);
|
|
createVMulF16WithConst(2, 2.0f, 1);
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
EXPECT_EQ(countInstructions(), 1);
|
|
|
|
auto& inst = *bb->begin();
|
|
auto* mul = static_cast<StinkyInstruction*>(&inst);
|
|
EXPECT_EQ(mul->getUnifiedOpcode(), static_cast<uint16_t>(GFX::v_mul_f16));
|
|
EXPECT_EQ(mul->getDestRegs()[0].reg.idx, 2u);
|
|
EXPECT_EQ(mul->getSrcRegs()[1].reg.idx, 0u);
|
|
}
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, NoMovPropagation_MultipleUses) {
|
|
// v_mov_b32 v1, v0
|
|
// v_add_f32 v2, 2.0, v1
|
|
// v_mul_f32 v3, 3.0, v1 // v1 used twice!
|
|
// => Should NOT propagate
|
|
|
|
createVMovB32(1, 0);
|
|
createVAddF32WithConst(2, 2.0f, 1);
|
|
createVMulF32WithConst(3, 3.0f, 1);
|
|
|
|
ASSERT_EQ(countInstructions(), 3);
|
|
|
|
runPass();
|
|
|
|
// Should still have 3 instructions
|
|
EXPECT_EQ(countInstructions(), 3);
|
|
}
|
|
|
|
TEST_F(PeepholePassManagerTest, IntegrationWithPassManager) {
|
|
// Test that the pass integrates correctly with PassManager
|
|
createVFmaF32WithConst(0, 1, 2, 1.0f);
|
|
createVAddF32WithConst(0, 1.0f, 0);
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
addPass(createBuildUseDefChainPass(true));
|
|
addPass(createPeepholeOptimizationPass());
|
|
run(func);
|
|
|
|
EXPECT_EQ(countInstructions(), 1);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Test: HexLiteral x FloatLiteral Constant Folding
|
|
// ============================================================================
|
|
|
|
TEST_F(PeepholeOptimizationPassTest, HexLiteralTimesFloatLiteral_MulMulFusion) {
|
|
// Test constant folding with hex literal (0x40ec7326) x float literal (2.0)
|
|
//
|
|
// Simulates the intrinsic flow:
|
|
// Intrinsic level:
|
|
// temp = v_mul_f32(src, 0x40ec7326) // HexLiteral
|
|
// dest = v_mul_f32(temp, 2.0) // FloatLiteral
|
|
//
|
|
// After lowering to Assembly IR:
|
|
// temp = v_mul_f32(src, 7.3890562) // Both become LiteralDouble
|
|
// dest = v_mul_f32(temp, 2.0)
|
|
//
|
|
// After peephole optimization (MUL+MUL fusion):
|
|
// dest = v_mul_f32(src, 14.778112) // ? Folded correctly
|
|
// // ? But hex format lost
|
|
//
|
|
// 0x40ec7326 = 7.3890562 (e^2 in IEEE 754)
|
|
// 7.3890562 * 2.0 = 14.778112
|
|
|
|
// Create the pattern using LiteralDouble (simulates post-lowering state)
|
|
// In real flow, IntrinsicOperand::HexLiteral gets converted to StinkyRegister::LiteralDouble
|
|
|
|
// temp = v_mul_f32(src, 7.3890562) // Originally HexLiteral 0x40ec7326
|
|
uint32_t hexBits = 0x40ec7326;
|
|
float hexFloat = std::bit_cast<float>(hexBits);
|
|
EXPECT_NEAR(hexFloat, 7.3890562f, 0.0001f) << "Verify hex literal value is e^2";
|
|
|
|
createVMulF32WithConst(0, hexFloat, 1); // v0 = 7.3890562 * v1
|
|
|
|
// dest = v_mul_f32(temp, 2.0) // FloatLiteral
|
|
createVMulF32WithConst(2, 2.0f, 0); // v2 = 2.0 * v0
|
|
|
|
ASSERT_EQ(countInstructions(), 2);
|
|
|
|
runPass();
|
|
|
|
// After fusion: should fold constants
|
|
EXPECT_EQ(countInstructions(), 1);
|
|
|
|
auto& inst = *bb->begin();
|
|
auto* mul = static_cast<StinkyInstruction*>(&inst);
|
|
ASSERT_NE(mul, nullptr);
|
|
|
|
EXPECT_EQ(mul->getUnifiedOpcode(), static_cast<uint16_t>(GFX::v_mul_f32));
|
|
EXPECT_EQ(mul->getDestRegs()[0].reg.idx, 2u); // Destination is v2
|
|
EXPECT_EQ(mul->getSrcRegs()[1].reg.idx, 1u); // Source is v1
|
|
EXPECT_EQ(mul->getSrcRegs()[0].dataType, StinkyRegister::Type::LiteralDouble);
|
|
|
|
// Verify constant folding: 7.3890562 * 2.0 = 14.778112
|
|
float expectedResult = hexFloat * 2.0f;
|
|
EXPECT_NEAR(expectedResult, 14.778112f, 0.0001f);
|
|
EXPECT_NEAR(mul->getSrcRegs()[0].literalDouble, expectedResult, 0.0001f)
|
|
<< "Constant folding should compute: 7.3890562 * 2.0 = 14.778112";
|
|
|
|
// Note: Hex format is lost - result is stored as decimal LiteralDouble
|
|
// Original: 0x40ec7326 (hex) -> 7.3890562 (decimal) -> 14.778112 (decimal)
|
|
// Not: 0x40ec7326 (hex) -> 0x416c73de (result in hex format)
|
|
}
|