Files
TheRock/rocm-libraries/shared/stinkytofu/tests/unit/asm/PeepholeOptimizationPassTest.cpp
T
derek.holloway 7502bf64a4
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
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

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)
}