/* ************************************************************************ * 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 #include #include #include #include #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 func; BasicBlock* bb; std::unique_ptr 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("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(&inst); ASSERT_NE(fma, nullptr); EXPECT_EQ(fma->getUnifiedOpcode(), static_cast(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(&inst); ASSERT_NE(fma, nullptr); EXPECT_EQ(fma->getUnifiedOpcode(), static_cast(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(&inst); ASSERT_NE(fma, nullptr); EXPECT_EQ(fma->getUnifiedOpcode(), static_cast(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(&inst); ASSERT_NE(fma, nullptr); EXPECT_EQ(fma->getUnifiedOpcode(), static_cast(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(&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(&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(&*it); EXPECT_EQ(fma->getUnifiedOpcode(), static_cast(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(&(*it)); EXPECT_EQ(fma->getUnifiedOpcode(), static_cast(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(&(*it)); EXPECT_EQ(fma->getUnifiedOpcode(), static_cast(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(&inst); ASSERT_NE(mul, nullptr); EXPECT_EQ(mul->getUnifiedOpcode(), static_cast(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(&inst); EXPECT_EQ(mul->getUnifiedOpcode(), static_cast(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(&inst); EXPECT_EQ(mul->getUnifiedOpcode(), static_cast(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(&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(&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(&(*it)); EXPECT_EQ(fma_result->getUnifiedOpcode(), static_cast(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(&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(&inst); EXPECT_EQ(fma_result->getUnifiedOpcode(), static_cast(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(&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(&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(&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(&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(&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(&inst); EXPECT_EQ(add->getUnifiedOpcode(), static_cast(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(&inst); EXPECT_EQ(add->getUnifiedOpcode(), static_cast(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(&inst); EXPECT_EQ(mul->getUnifiedOpcode(), static_cast(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(&inst); EXPECT_EQ(mul->getUnifiedOpcode(), static_cast(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(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(&inst); ASSERT_NE(mul, nullptr); EXPECT_EQ(mul->getUnifiedOpcode(), static_cast(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) }